TL;DR
Frontend architecture is the difference between shipping features and fighting your codebase. The decisions that matter: design system strategy (build vs adopt), component API patterns (composition over configuration), CSS approach (Tailwind + headless primitives), state management (optimistic UI + Server Components), and animation philosophy (spring physics, not linear easing). I have built frontend systems serving 100,000+ monthly active users across SaaS, fintech, and e-commerce. The patterns in this guide represent what actually works at scale... not theory, but battle-tested architecture decisions that compound into velocity.
Key Takeaways: Design system investment reduces design-to-code translation time by 70%. React Server Components cut JavaScript bundles from 400KB+ to under 100KB, dropping Time to Interactive from 2.4 seconds to 0.8 seconds. Migrating from CSS-in-JS to Tailwind plus headless primitives reduced one team's CSS bundle from 187KB to 12KB. Retrofitting accessibility costs 10x what building it in from day one costs. Use headless primitives (Radix) plus Tailwind for the best balance of performance, customization, and accessibility.
Why Frontend Architecture Matters
Here is a pattern I see repeatedly: a startup ships an MVP in 3 months. By month 12, feature velocity has dropped 60%. By month 18, developers are spending more time fighting the codebase than building features. By month 24, someone proposes a rewrite.
The cause is almost never framework choice or technology selection. It is frontend architecture... or more precisely, the absence of it.
Frontend architecture is the set of decisions about how your UI layer is structured, styled, and composed. These decisions compound. Good architecture creates velocity that accelerates over time. Poor architecture creates friction that accumulates until the codebase becomes hostile.
I have audited codebases where the same "card" component exists in 14 variations across the application... each with slightly different padding, border-radius, and shadow combinations. Not because anyone wanted 14 variations. Because there was no single source of truth. Because no one made the foundational decisions.
The stakes are concrete:
- Design system investment reduces design-to-code translation time by 70%
- Component API patterns determine whether developers love or avoid your component library
- CSS strategy affects bundle size by 10x and Time to Interactive by 300ms+
- State management patterns determine perceived performance (100ms feels instant; 1 second breaks flow)
- Server Components can reduce JavaScript bundles from 400KB+ to under 100KB
The cost of ignoring architecture is not theoretical. I have seen teams burn $500,000+ on rewrites that proper upfront architecture would have prevented. I have seen enterprise deals lost because accessibility was bolted on too late. I have seen engineers quit because fighting the codebase became their primary job.
This guide covers each of these decisions in depth, with links to detailed implementation guides for each topic.
Design System Strategy
The first architectural decision: how will your team create and maintain visual consistency?
The Build vs. Adopt Spectrum
Every team sits somewhere on this spectrum:
| Approach | Initial Investment | Customization | Long-term Velocity |
|---|---|---|---|
| Full custom | 3-6 months | Unlimited | Highest (if done well) |
| Headless + Tailwind | 1-2 weeks | High | High |
| Component library | 1-2 days | Low-medium | Medium |
| No system | 0 | Per-component | Lowest (compounding debt) |
I have advised startups at every point on this spectrum. The right choice depends on three factors:
Team size and design resources: Under 5 engineers with no dedicated designer? Use a component library. You will fight it eventually, but the initial velocity is worth it. Above 10 engineers with a design team? Build custom with headless primitives. The customization ceiling of component libraries will become painful.
Product category: Internal tools and admin dashboards? Component libraries are fine... users expect familiar patterns, and performance matters less. Consumer-facing products where brand differentiation matters? Build custom. Every SaaS looks the same when using Material UI.
Timeline to market: Raising a seed round and need to ship in 6 weeks? Component library. Building for the long term with runway? Invest in custom.
The Headless + Tailwind Sweet Spot
For most startups I advise, the winning strategy is headless primitives (Radix, Headless UI) plus Tailwind CSS plus shadcn/ui as a starting point.
This approach gives you:
- Accessibility built-in: Radix handles focus management, ARIA attributes, keyboard navigation. These are genuinely hard problems that most teams get wrong when building from scratch.
- Zero runtime overhead: No CSS-in-JS runtime, no style computation at runtime. Static CSS that purges to ~10KB.
- Full customization: You own the code. When your design diverges from the starting point, you modify your components... no fighting library internals.
- Velocity without lock-in: shadcn/ui gives you working components in minutes. Unlike npm dependencies, you can modify them freely.
For the technical implementation, see Neo-Brutalism: A Developer's Guide to Anti-Generic Design... it covers the specific patterns for implementing a distinctive visual language with Tailwind, including the hard shadows, thick borders, and intentional imperfection that define the neo-brutalist aesthetic.
When Component Libraries Make Sense
I want to be clear: component libraries are not inherently bad. They are a trade-off. The scenarios where they win:
Internal tools and admin dashboards: Users of internal tools expect familiar patterns. A settings page that looks like every other settings page is a feature, not a bug. Performance matters less because users are captive... they will wait 500ms longer if the alternative is not doing their job. Use Material UI, Ant Design, or Chakra and ship in 2 weeks instead of 2 months.
Rapid validation: If you are testing product-market fit and expect to throw away 80% of what you build, optimize for speed. A component library lets you validate ideas before investing in custom infrastructure.
Teams without design resources: If you have no designer and no design system, a component library provides cohesion you would not otherwise have. It is better than every developer implementing "buttons" differently.
Enterprise B2B: Enterprise buyers often prefer familiar interfaces. "It looks like Salesforce" can be a selling point.
The mistake is using a component library when you need brand differentiation, have performance requirements, or plan to build for the long term. The customization cliff... where the library does 80% of what you need but the last 20% requires fighting internals... arrives faster than teams expect.
Design Tokens: The API Layer
A design system without design tokens is a design system waiting to drift. Tokens are the contract between design and development.
Most token implementations stop at colors. This is insufficient. A complete token system covers:
- Typography: Font families, sizes, weights, line heights, letter spacing... as composite tokens
- Spacing: 8-point grid with semantic naming (inset, stack, inline)
- Elevation: Shadow + z-index bundles that encode visual hierarchy
- Motion: Duration + easing pairings that ensure consistent animation feel
The architecture matters: primitives (raw values) -> semantic tokens (meaning) -> component tokens (specific applications). This layering enables powerful changes... adjust one semantic token, and every component using it updates.
Consider this scenario: your design team decides that "medium" padding should increase from 16px to 20px across the application. Without token architecture, you search and replace across hundreds of files, hope you found everything, and break several things. With proper tokens, you change one value: space.inset.md: '20px'. Every button, card, and form using that token updates instantly.
The ROI is measurable. Teams I have worked with report 70% reduction in design-to-code translation time after implementing comprehensive tokens. The "that's not quite right" conversations disappear. Designers say "use space.inset.md" instead of "16 pixels... wait, is it 16 or 20?" And developers implement exactly what was specified.
For the complete token architecture... including typography scales, spacing systems, elevation hierarchies, and Figma integration... see Design Tokens Beyond Color: Typography, Spacing, and Elevation.
Component Architecture
Components are the unit of composition in modern frontend. Their APIs determine developer experience for the lifetime of your application.
The Props Hierarchy
Every component API is a contract. Get it wrong, and you create friction on every usage. Get it right, and components become intuitive to consume.
Minimal required props: If a prop has a sensible default, make it optional. Requiring unnecessary props creates boilerplate at every call site.
// Good: Required props are essential
interface ButtonProps {
children: React.ReactNode; // What does the button say?
onClick?: () => void; // Not all buttons need handlers
variant?: "primary" | "secondary" | "ghost"; // Defaults work
}
// Bad: Too many required props
interface ButtonProps {
children: React.ReactNode;
onClick: () => void; // Why required? Submit buttons don't need it
variant: "primary" | "secondary" | "ghost"; // Why required?
}
Discriminated unions for variants: When different variants have different valid props, TypeScript can enforce this at compile time:
type ButtonProps =
| { as: 'button'; onClick?: () => void; type?: 'button' | 'submit' }
| { as: 'a'; href: string; target?: '_blank' | '_self' }
| { as: 'link'; to: string }; // React Router
// TypeScript enforces correct combinations
<Button as="a" href="/about" /> // Valid
<Button as="a" onClick={() => {}} /> // Error: onClick not valid for 'a'
This eliminates entire categories of runtime errors. The compiler prevents invalid states.
Compound Components for Complex Structures
Simple components take props. Complex components need structure.
When a single component grows to 15+ props, it is time to refactor to compound components:
// Before: Prop explosion
<Card
title="User Profile"
titleSize="lg"
subtitle="Manage your account"
headerActions={<Button size="sm">Edit</Button>}
footer={<CardFooter />}
footerAlignment="right"
padding="lg"
>
<UserDetails />
</Card>
// After: Composition
<Card padding="lg">
<Card.Header>
<Card.Title size="lg">User Profile</Card.Title>
<Card.Description>Manage your account</Card.Description>
<Card.Actions>
<Button size="sm">Edit</Button>
</Card.Actions>
</Card.Header>
<Card.Body>
<UserDetails />
</Card.Body>
<Card.Footer alignment="right">
<Button variant="ghost">Cancel</Button>
<Button>Save Changes</Button>
</Card.Footer>
</Card>
The structure is visible. Customization is straightforward. The API scales.
The implementation uses React Context to share state between subcomponents:
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error("Tabs components must be used within <Tabs>");
}
return context;
}
That error message is crucial. When developers misuse the API, they get a clear explanation. This is API design... every detail matters.
For the complete patterns... including discriminated unions, render props, controlled vs uncontrolled components, and TypeScript generics for type-safe data... see Component API Design: Props, Variants, and Composition Patterns.
Accessibility by Default
Components must be accessible by default. Developers consuming your components should not need to think about accessibility... it should be baked in.
The economics are clear: retrofitting accessibility costs 10x what building it right costs. I have seen startups spend $180,000 in engineering time scrambling to meet accessibility requirements for an enterprise contract. That same investment upfront would have been 2 weeks of work.
Key patterns:
- Native HTML first: Use
<button>instead of<div role="button">. Native elements come with keyboard handling and screen reader support for free. - ARIA when necessary: For complex widgets (accordions, tabs, comboboxes), ARIA attributes communicate state to assistive technologies.
- Focus management: Every interactive element must be keyboard accessible, focus must be visible, and focus must be restored appropriately after interactions.
- Color + indicator: Never use color as the only means of conveying information.
Automated testing with axe-core catches 30-50% of accessibility issues. Manual screen reader testing catches the rest. CI/CD enforcement prevents regressions.
The testing stack I recommend:
- jest-axe for component-level testing: run accessibility checks on every component variant
- Playwright with @axe-core/playwright for end-to-end testing: verify accessibility after user interactions
- Storybook accessibility addon for development-time feedback: catch issues before they enter the codebase
- ESLint jsx-a11y for static analysis: catch missing alt text and improper element usage in the editor
Block merges that introduce violations. Accessibility is not optional... treat it like you treat type errors.
For implementation details, testing strategies, and WCAG compliance, see Accessibility in Design Systems: Testing and Enforcement.
CSS Strategy Decision Matrix
CSS architecture is often treated as an afterthought. This is a mistake. Your CSS strategy affects:
- Bundle size: From ~10KB (Tailwind purged) to 200KB+ (Ant Design)
- Runtime performance: From 0ms overhead (static CSS) to 200-300ms added TTI (CSS-in-JS)
- Customization: From "full control" to "fighting the library"
- Developer experience: From "intuitive" to "specificity wars"
The Four Approaches
| Approach | Bundle Size | Runtime Overhead | Customization | Best For |
|---|---|---|---|---|
| Utility-first (Tailwind) | ~10KB (purged) | Zero | Full | Teams with design system discipline |
| CSS-in-JS (Emotion, Styled Components) | Medium | 150-300ms added TTI | Full (dynamic styles) | Complex dynamic styling, at the cost of SSR complexity |
| CSS Modules | Small | Zero | Good encapsulation | Teams wanting scoped styles without runtime cost |
| Component libraries (Material UI, Chakra) | 50-200KB+ | Varies | Low-medium | Fast starts, but expensive to customize later |
The Decision Framework
| Context | Recommendation | Why |
|---|---|---|
| Internal tools, admin dashboards | Component library | Speed matters more than bundle size |
| Consumer-facing, brand-critical | Tailwind + custom | Full control, minimal bundle |
| Enterprise B2B | Component library | Familiar patterns build trust |
| Performance-critical (e-commerce, media) | Tailwind + custom | Every 100ms affects conversion |
| Rapid prototyping | Component library | Replace later if needed |
| Long-term product | Tailwind + headless | Stability without lock-in |
The shadcn/ui Synthesis
shadcn/ui represents the best current synthesis: accessible primitives (Radix), zero runtime (Tailwind), full customization (you own the code).
npx shadcn-ui@latest add button
This creates a file in your project that you own. When your design diverges, you modify your code... not fight library internals. No specificity wars. No vendor lock-in.
For the complete comparison with benchmarks and migration strategies, see Tailwind vs. Component Libraries: A Performance Deep Dive.
The Performance Reality
Let me share concrete numbers from a recent migration I led:
| Metric | Before (Ant Design + Emotion) | After (Tailwind + Radix + Custom) | Improvement |
|---|---|---|---|
| CSS bundle (gzipped) | 187KB | 12KB | 93% smaller |
| JavaScript bundle (gzipped) | 312KB | 89KB | 71% smaller |
| Time to Interactive (4G) | 2.4 seconds | 0.8 seconds | 3x faster |
| First Contentful Paint | 1.8 seconds | 0.6 seconds | 3x faster |
The migration took 6 weeks with a team of 3. The performance gains were permanent. Every page, every user, forever.
This is not a Tailwind advertisement... it is a demonstration that CSS architecture decisions have real performance consequences. Choose deliberately.
Designer-Developer Workflow
The "handoff" metaphor implies a relay race: designer finishes, passes baton, developer runs their leg. This model has fundamental problems.
The baton is a static artifact. The moment it is passed, it starts diverging from reality. The designer iterates. The developer interprets. The production code drifts.
Continuous Synchronization
Modern design-development workflows replace handoff with continuous synchronization:
Design tokens as shared source of truth: Colors, spacing, typography defined in Figma Variables, exported to JSON (DTCG format), transformed by Style Dictionary into CSS custom properties and Tailwind config. Change a token in Figma, and a GitHub Action creates a PR.
Figma Code Connect: Map Figma components to production code. When developers inspect a button in Figma Dev Mode, they see actual React component syntax... not generic CSS approximations.
// figma.config.tsx
figma.connect(Button, "https://figma.com/...", {
props: {
variant: figma.enum("Variant", {
Primary: "primary",
Secondary: "secondary",
}),
},
example: (props) => <Button variant={props.variant}>{props.label}</Button>,
});
Visual regression testing: Percy or Chromatic captures screenshots on every PR. Changes are reviewed before merge. This catches drift before it reaches users.
The Definition of Done
Replace "pixel perfect" (technically impossible) with meaningful criteria:
- Visual regression approved: VRT passes... change is intentional
- Accessibility audit passed: WCAG AA compliance, keyboard navigation works
- Responsive verification: Tested at all defined breakpoints
- State coverage: All interactive states implemented (hover, focus, disabled, loading, error)
- Token compliance: All values use design tokens, no magic numbers
For the complete workflow including token pipelines and testing setup, see The Designer-Developer Handoff: From Friction to Flow.
The Anti-Pattern: Review by Screenshare
Here is the workflow I see at struggling teams:
- Designer creates mockups in Figma
- Designer screenshares with developer: "Make it look like this"
- Developer interprets, implements
- Designer reviews: "That padding is wrong. The shadow is too heavy."
- Developer adjusts
- Designer: "Closer, but the hover state is different"
- Repeat until someone gives up
Each round takes 30-60 minutes. Three rounds is common. That is 2+ hours per component. At 50 components in a typical design system, you have burned 100 hours on "handoff."
The alternative: designer specifies space.inset.md and elevation.raised. Developer implements exactly that. Done in 5 minutes. No screenshare. No interpretation. No iteration.
Token-based communication is 10x faster than pixel-based communication. This is not marginal improvement... it is a category change.
State Management Patterns
State management architecture determines perceived performance. 100ms feels instant. 1 second breaks cognitive flow. 10 seconds causes abandonment.
Optimistic UI: The Productive Lie
Traditional request-response: User clicks -> spinner -> API call -> response -> UI update. Even a fast 200ms API response feels sluggish when combined with network latency and render time.
Optimistic UI inverts this: User clicks -> UI updates immediately -> API call fires in background -> if failure, rollback.
const mutation = useMutation({
mutationFn: api.addTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previousTodos = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old) => [...old, newTodo]);
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
toast.error("Failed to save");
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
The user sees instant feedback. The "system think time" overlaps with the user's "what's next" think time.
When NOT to use optimistic UI:
- Financial transactions: Never show "Transfer Complete" before the server confirms
- Scarce inventory: "You got the last ticket" -> refresh -> "Sorry, sold out" creates worse UX than a brief wait
- Irreversible destructive actions: Optimistically showing "Server Deleted" when the API might fail creates ambiguity
For the complete patterns including conflict resolution and error handling, see Optimistic UI: Making Apps Feel Faster Than Physics Allows.
Server Components: The Waterfall Killer
Traditional SPA loading sequence:
- HTML shell downloads
- JavaScript bundle downloads
- React hydrates, shows loading state
- Client fetches data from API
- React re-renders with data
Each step blocks the next. On slow connections, 3-5 seconds to interactive is common.
React Server Components invert this:
// Server Component - runs on server, ships zero JS
async function PostPage({ postId }: { postId: string }) {
const post = await db.posts.findUnique({ where: { id: postId } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={postId} /> {/* Client Component island */}
</article>
);
}
The page is mostly server-rendered HTML. Only interactive islands ship JavaScript. Combined with edge deployment, Time to First Byte drops from 200ms+ to under 50ms globally.
Bundle size impact:
- Before RSC: 400KB+ JavaScript (React, data fetching, components)
- After RSC: Often under 100KB (only Client Components)
For implementation details, migration strategies, and edge deployment patterns, see RSC, The Edge, and the Death of the Waterfall.
The Mental Model Shift
The key to Server Components is understanding what runs where:
| Capability | Server Components (default) | Client Components ('use client') |
|---|---|---|
| Execution | Runs during server render | Runs in the browser |
| Data fetching | Can fetch directly from databases, file systems, or APIs | Requires useEffect or data fetching library |
| React hooks | Cannot use useState, useEffect, etc. | Full hook support |
| Browser APIs | Cannot access window, document | Full browser API access |
| JavaScript shipped | Zero JS sent to browser | Ships JavaScript to browser |
| Usage guidance | Default for all components | Use sparingly... only where interactivity requires it |
The composition pattern is powerful:
// Server Component - fetches data, ships no JS
async function ProductPage({ id }: { id: string }) {
const product = await db.products.findUnique({ where: { id } });
return (
<main>
<ProductDetails product={product} /> {/* Server */}
<ProductReviews productId={id} /> {/* Server, can be Suspense */}
<AddToCartButton productId={id} /> {/* Client island */}
</main>
);
}
The page is 90% HTML. Only the Add to Cart button ships JavaScript. This is the correct default for most pages.
Animation and Performance
Animation is not decoration... it is communication. State changes, spatial relationships, causality, errors. The physics of motion create the illusion that UI elements have weight.
Spring Physics, Not Linear Easing
Biological motion follows physics. When you reach for a coffee cup, your arm accelerates, decelerates, and settles... with a tiny overshoot if you are rushed.
Linear animations violate this intuition. They feel robotic because nothing in nature moves linearly.
Spring animations model physical oscillation. The key parameter is damping ratio:
- Underdamped (< 1): Bouncy, overshoots and oscillates. Use for playful interactions.
- Critically damped (= 1): Fastest path to rest without overshoot. Use for most UI animations.
- Overdamped (> 1): Sluggish approach. Rarely appropriate.
// Critically damped - most UI work
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
/>
Layout Projection
Animating width and height triggers layout recalculation, which blocks the main thread. Framer Motion's Layout Projection uses the FLIP technique to animate these properties performantly using GPU-accelerated transforms.
<motion.div layout>{/* Content that changes size */}</motion.div>
Accessibility: Reduced Motion
Some users experience motion sickness triggered by animation. The prefers-reduced-motion media query lets users opt out.
const prefersReduced = usePrefersReducedMotion();
<motion.div
initial={{ opacity: 0, y: prefersReduced ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={prefersReduced ? { duration: 0 } : { type: "spring", stiffness: 200, damping: 20 }}
/>;
For the complete physics model, velocity preservation, and performance optimization, see Atmospheric Animations: The Physics of Framer Motion.
Motion as Communication
Animation communicates things words and static design cannot:
Causality: When a button press triggers a modal, the modal should emerge from the button location. This shows the user what caused the change.
Spatial relationships: When navigating between pages, consistent directional motion (forward = slide left, back = slide right) creates a mental map of application structure.
State changes: A loading spinner communicates "working." A checkmark fade-in communicates "done." A shake communicates "error." These are faster to process than text.
Hierarchy: Elements with more motion draw attention. Use this deliberately... emphasize what matters, let background elements be static.
The trap is treating animation as decoration. If you are animating something without a communication purpose, you are adding latency without value. Every animation should answer: "What is this telling the user?"
Architecture Decision Checklist
Before starting a new frontend project, answer these questions:
Design System
- Build vs. adopt: Based on team size, design resources, and timeline?
- Token architecture: Typography, spacing, elevation, motion... not just colors?
- Component strategy: Headless primitives + Tailwind, or component library?
- Accessibility: Built-in from day one, with testing and enforcement?
CSS Strategy
- Bundle impact: Understood the size and runtime cost?
- Customization needs: Can the approach support your design?
- Developer experience: Is the approach sustainable for your team?
- Performance targets: TTI under 100ms on target devices?
Component Patterns
- API design: Minimal required props, discriminated unions for variants?
- Composition model: Compound components for complex structures?
- Controlled vs. uncontrolled: Explicit choice, preferably supporting both?
- TypeScript integration: Generics for type-safe data, inference over annotation?
State Management
- Optimistic UI: For appropriate operations (not financial, not scarce)?
- Server Components: For data fetching and static content?
- Cache strategy: React Query, SWR, or local-first?
- Error handling: Rollback, retry, user feedback?
Workflow
- Token pipeline: Figma -> JSON -> CSS/Tailwind automated?
- Code Connect: Figma components linked to production code?
- Visual regression: Percy or Chromatic in CI?
- Definition of done: Accessibility, responsiveness, state coverage?
Spoke Articles in This Series
This hub page provides the strategic overview. Each spoke article goes deep on implementation:
Design System Foundation
-
Neo-Brutalism: A Developer's Guide to Anti-Generic Design: Philosophy and implementation of distinctive visual language with Tailwind. Hard shadows, thick borders, the neo-brutalist aesthetic.
-
Design Tokens Beyond Color: Typography, Spacing, and Elevation: Complete token architecture from primitives to component tokens. Figma Variables integration, Style Dictionary pipeline.
-
Accessibility in Design Systems: Testing and Enforcement: ARIA patterns, focus management, automated testing with axe-core, CI/CD enforcement.
Component Architecture
-
Component API Design: Props, Variants, and Composition Patterns: Discriminated unions, compound components, controlled vs. uncontrolled, TypeScript integration.
-
The Designer-Developer Handoff: From Friction to Flow: Figma Code Connect, token pipelines, visual regression testing, the new definition of done.
CSS and Styling
- Tailwind vs. Component Libraries: A Performance Deep Dive: Bundle size analysis, runtime overhead, the customization cliff, decision framework.
State and Data
-
Optimistic UI: Making Apps Feel Faster Than Physics Allows: React Query patterns, conflict resolution, when NOT to use optimistic UI.
-
RSC, The Edge, and the Death of the Waterfall: Server Components mental model, edge deployment, streaming, migration strategy.
TypeScript
- TypeScript: The Business Case for Static Types: 15-38% of bugs preventable with types. ROI analysis, refactoring velocity, onboarding impact.
Motion and Feel
- Atmospheric Animations: The Physics of Framer Motion: Spring physics, damping ratio, layout projection, velocity preservation, reduced motion accessibility.
Implementation Roadmap
If you are starting from scratch, here is the sequence I recommend:
Week 1: Foundation
- Token architecture: Define primitives for spacing, typography, color, shadow, motion
- Tailwind config: Integrate tokens into Tailwind
- Base components: Button, Input, Card using shadcn/ui as starting point
- Accessibility baseline: axe-core tests, focus states
Week 2: Workflow
- Figma setup: Variables matching your tokens
- Token pipeline: Style Dictionary config, GitHub Action for sync
- Storybook: Component documentation with a11y addon
- Visual regression: Chromatic or Percy integration
Week 3: State
- Data layer: React Query or SWR setup
- Optimistic patterns: For appropriate mutations
- Server Components: Migrate data fetching from useEffect
- Error boundaries: Graceful failure handling
Week 4: Polish
- Animation system: Spring presets, reduced motion support
- Complex components: Modals, dropdowns, tabs with proper ARIA
- Documentation: Usage examples, decision records
- Performance audit: Lighthouse, bundle analysis, real user monitoring
Conclusion
Frontend architecture is not about choosing the "right" framework or following "best practices." It is about making deliberate decisions that compound into velocity.
The decisions that matter:
- Design system strategy: Build custom with headless primitives for consumer-facing products, adopt component libraries for internal tools
- Component API patterns: Composition over configuration, discriminated unions for type safety, accessibility by default
- CSS approach: Tailwind + headless primitives for performance and customization, component libraries only when velocity matters more than bundle size
- State management: Optimistic UI for perceived performance, Server Components for actual performance
- Animation philosophy: Spring physics creates natural motion, reduced motion respects accessibility
These are not theoretical preferences. They are patterns distilled from building production systems serving 100,000+ users. The failures I have seen... the rewrites, the 60% velocity drops, the accessibility lawsuits... came from ignoring these fundamentals.
The investment in architecture pays for itself. 2-3 weeks of foundation work returns months of accelerated delivery. Design-to-code translation time drops 70%. Accessibility issues drop 40%. Bundle sizes shrink by 4x.
Build the foundation. Make the decisions explicit. Then ship features.
Frequently Asked Questions
Should I use React Server Components in production?
Yes, if you are on Next.js 14+. React Server Components reduce client-side JavaScript by 30-60% by rendering data-fetching components on the server. They eliminate client-server waterfalls for data loading. The main constraint is that Server Components cannot use hooks, browser APIs, or event handlers ... those require Client Components with the 'use client' directive.
How do I choose between Tailwind CSS and component libraries?
Use Tailwind CSS when you need full design control and have designers creating custom interfaces. Use component libraries (Radix, shadcn/ui) when you need accessible, tested components without building from scratch. The best approach for most teams: Tailwind for styling + headless component libraries (Radix primitives) for complex interactive patterns like modals, dropdowns, and comboboxes.
What is a design token system and when do I need one?
Design tokens are the single source of truth for visual design decisions: colors, spacing, typography, shadows, and animations stored as platform-agnostic variables. You need a token system when your design starts having inconsistencies across components, or when you have 2+ developers touching UI code. Tokens reduce design debt by making it impossible to use unauthorized values.
How do I improve Core Web Vitals for a React application?
Focus on the three metrics: LCP (Largest Contentful Paint) under 2.5 seconds, INP (Interaction to Next Paint) under 200ms, and CLS (Cumulative Layout Shift) under 0.1. The highest-impact fixes are: lazy-load below-fold images, use next/image for automatic optimization, code-split routes with dynamic imports, and set explicit dimensions on images and embeds to prevent layout shifts.
What is the best state management approach for React in 2026?
Server state (TanStack Query or SWR) for API data, URL state for navigation and filters, and minimal client state (Zustand or React Context) for UI-only concerns. Most applications over-use global state. If your data comes from an API, it is server state and should be managed by a server-state library that handles caching, revalidation, and optimistic updates automatically.
Ready to implement production-grade frontend architecture? I help teams build design systems, migrate to modern patterns, and establish workflows that accelerate delivery.
- React Development for SaaS - Component systems and modern React
- Next.js Development for SaaS - Server Components and edge deployment
- Full-Stack Development for Startups - End-to-end architecture
- Technical Advisor for Startups - Strategic guidance on architecture decisions
