Building Maintainable Design Systems in 2026: From Tokens to Semantic Layers and AI-Assisted Theming
The Figma → tokens → CSS variables pipeline was the right answer in 2022. In 2026, it breaks down under the weight of multi-brand theming, dark mode, and teams that need to ship variants without touching the design system core.
Our design system started like most: a Figma library, a token export, a pile of CSS custom properties, and a loose convention about which variables to use where. It worked well enough for one product and one brand. The moment we added a second brand, a dark mode, and a white-label offering, it collapsed under its own inconsistency.
Here is what we rebuilt, and why each layer of the new architecture exists.
The Problem With Primitive Tokens
Primitive tokens are named for their value: --color-blue-500, --spacing-4, --radius-md. They are the foundation of most design token systems and they work perfectly — as a foundation. The problem is when components reference them directly.
/* FRAGILE — component references primitive token */
.button-primary {
background: var(--color-blue-500);
color: var(--color-white);
}
/* A different brand needs green. Now you either:
1. Override --color-blue-500 (confusing — it is no longer blue)
2. Add a brand-specific stylesheet (token system defeats itself)
3. Wish you had used semantic tokens from the start */
Primitive tokens are a palette. They should never be referenced in components directly.
The Semantic Layer
Semantic tokens sit between primitives and components. They are named for their purpose, not their value:
/* tokens/semantic.css */
:root {
/* Map semantics to primitives — brand-neutral defaults */
--color-action-primary: var(--color-blue-500);
--color-action-primary-fg: var(--color-white);
--color-surface-default: var(--color-neutral-0);
--color-surface-subtle: var(--color-neutral-50);
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-600);
--color-border-default: var(--color-neutral-200);
}
/* Brand override — only touches semantic layer */
[data-brand="acme"] {
--color-action-primary: var(--color-green-600);
--color-action-primary-fg: var(--color-white);
}
/* Dark mode override — only touches semantic layer */
@media (prefers-color-scheme: dark) {
:root {
--color-surface-default: var(--color-neutral-950);
--color-text-primary: var(--color-neutral-50);
--color-border-default: var(--color-neutral-800);
}
}
Now components reference only semantic tokens. Rebranding means updating a handful of semantic mappings — not touching a single component.
Component-Level Tokens
The third layer is component tokens — local custom properties that expose the component's customization surface without exposing its internals:
/* components/Button/Button.css */
.button {
/* Component token defaults — fall back to semantic layer */
--button-bg: var(--color-action-primary);
--button-fg: var(--color-action-primary-fg);
--button-radius: var(--radius-md);
--button-padding: var(--spacing-2) var(--spacing-4);
background: var(--button-bg);
color: var(--button-fg);
border-radius: var(--button-radius);
padding: var(--button-padding);
}
/* Consumer overrides just the button — doesn't touch semantic layer */
.checkout-page .button {
--button-radius: var(--radius-full); /* pill shape in checkout */
}
Variants in Code, Not in CSS Classes
The traditional approach to component variants is a proliferation of modifier classes: .button--primary, .button--secondary, .button--danger, .button--sm, .button--lg. This works until you need combinations, and then you end up with 20 classes for a button.
We moved to a TypeScript-first variant model using cva (class-variance-authority):
// components/Button/Button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
// Base styles always applied
'inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
{
variants: {
intent: {
primary: 'bg-[--color-action-primary] text-[--color-action-primary-fg]',
secondary: 'bg-transparent border border-[--color-border-default]',
danger: 'bg-[--color-feedback-danger] text-white',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
},
},
defaultVariants: { intent: 'primary', size: 'md' },
}
);
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
& VariantProps<typeof buttonVariants>;
export function Button({ intent, size, className, ...props }: ButtonProps) {
return (
<button className={buttonVariants({ intent, size, className })} {...props} />
);
}
Every valid combination is type-safe. Consumers get autocomplete. Invalid combinations are caught at compile time.
AI-Assisted Theming
This is the part that genuinely surprised me in terms of practical utility. Our white-label offering requires generating custom themes from a brand's primary color — ideally without a designer having to manually derive every semantic token value.
We built a small internal tool that takes a brand hex color and uses an LLM to derive a coherent semantic token set. The prompt is carefully constrained:
// tools/generate-theme.ts
const systemPrompt = `
You are a design systems expert. Given a primary brand color in hex,
generate a semantic color token set for a web application.
Output ONLY valid JSON matching this schema:
{
"color-action-primary": string, // the brand color or a variant
"color-action-primary-hover": string, // slightly darker
"color-action-primary-subtle": string,// 10% opacity background tint
"color-feedback-danger": string, // red, harmonious with brand
"color-feedback-success": string // green, harmonious with brand
}
Rules:
- All colors must pass WCAG AA contrast against white or be marked as bg-only
- Do not invent colors unrelated to the brand hue
- Output raw JSON only, no explanation
`;
export async function generateTheme(brandColor: string) {
const response = await llm.chat({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `Brand color: ${brandColor}` },
],
});
return JSON.parse(response.content) as ThemeTokens;
}
The result is not perfect, but it produces an 80% correct theme in seconds. A designer then reviews and adjusts in a few minutes rather than deriving everything from scratch. The ROI is significant for a white-label product where themes are created frequently.
Toolchain
Our current stack for the token pipeline:
- Style Dictionary — transforms and exports tokens to CSS, JS, iOS, and Android formats from a single source
- Tokens Studio for Figma — syncs design tokens bidirectionally with the code repository
- cva — type-safe component variants in React
- Stylelint plugin (custom) — enforces semantic-only token usage in component CSS
- Chromatic — visual regression testing catches unintended token changes across the component library
Key Takeaways
- Primitive tokens are a palette — components must only reference semantic tokens
- Semantic tokens are the brand and theme customization surface — one mapping change rebrand everything
- Component tokens expose a clean customization API without exposing internals
- Use
cvaor equivalent for type-safe variant management — CSS modifier classes do not scale - LLMs can bootstrap themes usefully; always validate output against contrast requirements programmatically