Frontend Architecture for Design Systems
Why a solid frontend architecture is essential as design systems move from static libraries to dynamic, cross-platform products.

When I first joined a team tasked with building a design system, I thought it was about creating a set of reusable React components and a matching Figma library. We spent weeks debating the perfect button border-radius. Six months later, we had a beautiful component library that nobody used outside of the design team because integrating it into our existing monolithic frontend felt like forcing a square peg into a round hole. The JavaScript bundle was massive, the theming was rigid, and overriding styles required !important hacks.
That experience taught me that a design system is not just a UI kit. It is a product whose frontend architecture dictates its adoption, scalability, and longevity. In modern frontend development, where we support web, mobile web, server-side rendering (SSR), and even desktop wrappers, the architecture behind our design tokens and components must be as robust as the application logic itself.
This post explores the architectural decisions, patterns, and tools required to build a design system that survives contact with real-world codebases. We will move beyond "Atomic Design" theory and look at how to structure a monorepo, implement type-safe tokens, and distribute a package that works seamlessly across different JavaScript runtimes.
The Modern Context of Design Systems
Design systems have evolved from static style guides to dynamic design tokens that drive UI across multiple platforms. Today, a design system is rarely a web-only concern. It serves as the single source of truth for web apps (React, Vue, Svelte), native mobile apps (React Native, Swift, Kotlin), and sometimes even backend dashboards.
In this context, frontend architecture focuses on interoperability and distribution. We are no longer just writing CSS; we are managing data structures (tokens) that get transformed into platform-specific formats (CSS variables, JSON for iOS, XML for Android).
Who Needs This Architecture?
- Platform Teams: Teams maintaining shared libraries across multiple product squads.
- Frontend Engineers: Who need to enforce consistency without writing custom CSS for every feature.
- Designers: Who rely on engineering constraints to ensure designs are feasible.
Comparison with Alternatives
- Hardcoded Styles: Directly writing CSS/SCSS in apps is faster initially but creates massive tech debt. It lacks the systematic enforcement a design system provides.
- Third-Party Libraries (MUI, Chakra, Tailwind UI): These are excellent starting points but often become a ceiling for customization. If your brand identity requires highly specific interactions or animations, an in-house architecture is often necessary to avoid fighting the library's opinionation.
Core Architectural Concepts
The heart of a scalable design system is Design Tokens. These are platform-agnostic variables (colors, spacing, typography) that are transformed into platform-specific code.
The Token Pipeline
We need a build process that takes a source of truth (usually JSON or YAML) and outputs various formats. Without this, we end up duplicating values across CSS, JavaScript, and native configs.
Component Abstraction
Components should be "dumb" regarding styling details but "smart" regarding logic. They consume tokens via CSS Custom Properties (Variables) or a JavaScript context provider, rather than hard-coded hex values.
Technical Deep Dive: Building a Token-Driven System
Let's look at a practical implementation. We will use a monorepo structure (using Turborepo or Nx) to manage the design system and a demo app. This ensures that changes in the design system are immediately testable in a consumer app without publishing to npm.
Project Structure
Here is a typical folder structure for a robust design system architecture:
/design-system-monorepo
├── /apps
│ └── /docs # Storybook or documentation site
│ └── /example-next # Next.js app to test components
├── /packages
│ ├── /tokens # The source of truth (JSON tokens)
│ ├── /styles # Compiled CSS/SCSS (if using CSS-in-JS, this might be merged)
│ ├── /ui-react # React component library
│ └── /utils # Shared utilities (e.g., spacing calculation)
├── package.json
└── turbo.json # Pipeline orchestration
1. Defining Design Tokens (JSON Source of Truth)
We start with a tokens.json file in the packages/tokens directory. This file is the single source of truth.
// packages/tokens/tokens.json
{
"color": {
"brand": {
"primary": { "value": "#0052CC", "type": "color" },
"secondary": { "value": "#36B37E", "type": "color" }
},
"semantic": {
"error": { "value": "#FF5630", "type": "color" }
}
},
"spacing": {
"small": { "value": "4px", "type": "spacing" },
"medium": { "value": "8px", "type": "spacing" },
"large": { "value": "16px", "type": "spacing" }
}
}
2. Transforming Tokens with Style Dictionary
We use Style Dictionary (an Amazon open-source tool) to transform these JSON tokens into CSS, SCSS, and JavaScript objects.
Create a configuration file in packages/tokens/config.json:
// packages/tokens/config.json
{
"source": ["tokens.json"],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "build/css/",
"files": [{
"destination": "variables.css",
"format": "css/variables"
}]
},
"js": {
"transformGroup": "js",
"buildPath": "build/js/",
"files": [{
"destination": "tokens.js",
"format": "javascript/es6"
}]
}
}
}
Running the build generates files in the build/ folder. The CSS output looks like this:
/* packages/tokens/build/css/variables.css */
:root {
--color-brand-primary: #0052CC;
--color-brand-secondary: #36B37E;
--spacing-small: 4px;
--spacing-medium: 8px;
}
3. Consuming Tokens in React Components
In our packages/ui-react library, we don't hardcode colors. We consume the CSS variables or the JS constants.
Here is a robust Button component using TypeScript. Notice how it accepts standard props but allows for token-based overrides.
// packages/ui-react/src/Button/Button.tsx
import React from 'react';
import styles from './Button.module.css';
// We can import the JS tokens if we need logic based on values
// import { spacing } from '@my-org/tokens';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
disabled?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
onClick,
disabled = false
}) => {
// Mapping props to CSS class names
// In a more complex system, we might use a CSS-in-JS library like Stitches or Vanilla Extract
// to inject the token values directly.
const buttonClasses = [
styles.button,
styles[`variant-${variant}`],
styles[`size-${size}`],
disabled ? styles.disabled : ''
].join(' ');
return (
<button
className={buttonClasses}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
The corresponding CSS Module (Button.module.css) utilizes the variables generated by Style Dictionary.
/* packages/ui-react/src/Button/Button.module.css */
.button {
/* Using CSS Custom Properties injected by the tokens package */
border-radius: 4px;
cursor: pointer;
font-family: sans-serif;
transition: all 0.2s ease;
}
.variant-primary {
background-color: var(--color-brand-primary, #0052CC); /* Fallback value */
color: white;
border: none;
}
.variant-primary:hover:not(:disabled) {
opacity: 0.9;
}
.size-md {
padding: var(--spacing-medium) var(--spacing-large);
font-size: 14px;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
4. Asynchronous Token Loading (Advanced)
In some scenarios, tokens might be fetched dynamically (e.g., user-specific themes). While we prefer static generation for performance, we must handle async contexts. Here is a pattern using React Context to provide tokens at runtime.
// packages/ui-react/src/Theme/ThemeProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
// Mock async fetcher
const fetchThemeTokens = async (): Promise<Record<string, string>> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
'--color-brand-primary': '#6554C0', // Overridden color
'--spacing-large': '20px',
});
}, 500); // Simulate network latency
});
};
const ThemeContext = createContext<Record<string, string> | null>(null);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [tokens, setTokens] = useState<Record<string, string>>({});
useEffect(() => {
fetchThemeTokens().then((fetchedTokens) => {
setTokens(fetchedTokens);
// Inject into DOM as CSS variables
Object.entries(fetchedTokens).forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value);
});
});
}, []);
return (
<ThemeContext.Provider value={tokens}>
{children}
</ThemeContext.Provider>
);
};
export const useTokens = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTokens must be used within a ThemeProvider');
}
return context;
};
Honest Evaluation: Strengths and Weaknesses
Strengths
- Single Source of Truth: Changes to a color in
tokens.jsonpropagate to Web, iOS, and Android. This prevents the dreaded "design drift." - Scalability: By separating tokens from components, you can rewrite the component library (e.g., moving from React to Svelte) without losing the design logic.
- Developer Experience (DX): Using TypeScript with generated token types provides autocomplete for colors and spacing in the IDE.
Weaknesses
- Initial Overhead: Setting up Style Dictionary or Theo is complex. It requires build engineering skills that many frontend teams lack.
- Bundle Size: If you include the entire token set in the JS bundle, you might bloat the app. You need tree-shaking (supported by ESM exports).
- Rigidity: A strict system can feel stifling when designers need to break the rules for marketing pages. You need an "escape hatch" (e.g., raw CSS capability).
When to Use This Architecture
- Do use it if: You have multiple codebases (e.g., a marketing site in Next.js and an app in React Native) that must look identical.
- Skip it if: You are building a simple static landing page or a small internal tool. In that case, a utility-first CSS framework like Tailwind is faster and sufficient.
Personal Experience and Common Pitfalls
I have implemented design system architectures in three different companies, and the pitfalls were almost always organizational rather than technical.
The "Token Explosion" Trap
In one project, we allowed every possible variation of spacing (4px, 6px, 8px, 10px, 12px...). This defeated the purpose of a design system, which is to restrict choices to improve consistency.
Lesson: Be opinionated. If the design system allows spacing-large, it should not allow arbitrary pixel values. Enforce this with linting rules (e.g., Stylelint).
Failing at the Fallback
A common mistake in token-based CSS is forgetting fallback values.
background-color: var(--color-primary); is dangerous. If the variable isn't defined (perhaps in a legacy browser or during a loading state), the element renders transparent.
Fix: Always use the CSS var() syntax with fallbacks: var(--color-primary, #000).
The Value of TypeScript
When we first built our system without TypeScript, we constantly passed invalid color strings to components. Moving to a typed token system (generating .d.ts files from JSON) eliminated an entire class of bugs. It allowed us to treat design tokens as code, not just documentation.
Getting Started: Workflow and Tooling
To build this architecture, you don't need to start from scratch. Here is a mental model for setting up your workflow.
1. The Toolchain
- Repository Management: Use Turborepo. It handles the dependency graph between your
tokenspackage andui-reactpackage. - Token Transformation: Start with Style Dictionary. It is the industry standard and supports custom transforms.
- Component Library: Use Vite for building the library. It is significantly faster than Webpack for library mode.
- Documentation: Storybook is essential. It isolates components and allows you to visualize token changes instantly.
2. Setting up the Build Pipeline
In a monorepo, you need to define the build pipeline. Here is an example of a turbo.json configuration. This ensures that tokens are built before ui-react.
{
"$schema": "https://turborepo.com/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"tokens#build": {
"dependsOn": [],
"outputs": ["build/**"]
},
"ui-react#build": {
"dependsOn": ["tokens#build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
3. Handling Styling Strategies
When building components, you have three architectural choices:
- CSS Modules: (Used in the example above) Good for isolation, but hard to theme dynamically. Best for static apps.
- CSS-in-JS (e.g., Styled Components, Emotion): Great for dynamic theming (swapping themes at runtime) but adds runtime overhead and complexity to the build.
- Headless UI + Tailwind (or Vanilla Extract): A modern approach. Use libraries like Radix UI for accessibility/logic and apply tokens via CSS variables. This keeps bundle size low.
Free Learning Resources
To deepen your understanding of frontend architecture for design systems, these resources are invaluable:
- Style Dictionary Documentation: The official guide is the best place to understand how to transform JSON into various platform formats.
- Theo by Salesforce: Although Style Dictionary is more popular now, Theo pioneered the concept. Reading their documentation helps understand the history of token architecture.
- Figma Variables API: If you want to automate the flow from Design to Code (Design Tokens), you need to understand the Figma API.
- Material Design 3 Tokens: Google’s implementation is a masterclass in how to structure semantic tokens vs. primitive tokens.
- Building a Design System in React: A practical tutorial covering the nuances of component composition.
Conclusion
Building a frontend architecture for a design system is an investment in velocity and quality. It shifts the focus from "making things look right" to "building things the right way."
Who should use this architecture? Teams building products that span multiple platforms (web, mobile), teams experiencing rapid growth where inconsistency is creeping in, or teams simply tired of patching CSS bugs.
Who might skip it? Solo developers building a single-purpose application, or teams working on highly experimental prototypes where the cost of setting up the token pipeline outweighs the benefits.
The ultimate goal of this architecture is to make the design system invisible to the consumer. Developers should import a component, pass a few props, and trust that the styling adheres to the brand guidelines without writing a single line of CSS. When you achieve that, you have built a truly robust system.




