Foundational design system architecture: token hierarchy, theming infra, token pipelines, governance.
Foundational architecture for scalable design systems: token hierarchies, theming infrastructure, token pipelines, and governance patterns.
p-[13px], #123456) требуют объяснения/исключения.Three-layer token architecture separates raw values from meaning from usage.
/* Layer 1: Primitive tokens — raw values, never used directly in components */
:root {
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-gray-50: #fafafa;
--color-gray-900: #171717;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-4: 1rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
}
/* Layer 2: Semantic tokens — contextual meaning, theme-aware */
:root {
--text-primary: var(--color-gray-900);
--text-secondary: var(--color-gray-600);
--surface-default: white;
--surface-elevated: var(--color-gray-50);
--border-default: var(--color-gray-200);
--interactive-primary: var(--color-blue-500);
--interactive-primary-hover: var(--color-blue-600);
}
/* Layer 3: Component tokens — specific usage, optional */
:root {
--button-bg: var(--interactive-primary);
--button-bg-hover: var(--interactive-primary-hover);
--button-text: white;
--button-radius: var(--radius-md);
--button-padding-x: var(--space-4);
--button-padding-y: var(--space-2);
}
Semantic tokens are the most important layer — they enable theming. Component tokens are optional and useful for complex component libraries.
Key capabilities: theme (user selection), resolvedTheme (actual light/dark), setTheme, system preference detection, localStorage persistence, DOM attribute application.
type Theme = "light" | "dark" | "system";
export function ThemeProvider({ children, defaultTheme = "system", storageKey = "theme",
attribute = "data-theme" }: { children: React.ReactNode; defaultTheme?: Theme;
storageKey?: string; attribute?: "class" | "data-theme" }) {
const [theme, setThemeState] = useState<Theme>(() =>
typeof window === "undefined" ? defaultTheme
: (localStorage.getItem(storageKey) as Theme) || defaultTheme);
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
const getSystem = useCallback(() =>
matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" as const, []);
const apply = useCallback((r: "light" | "dark") => {
const root = document.documentElement;
attribute === "class"
? (root.classList.remove("light", "dark"), root.classList.add(r))
: root.setAttribute(attribute, r);
root.style.colorScheme = r;
setResolvedTheme(r);
}, [attribute]);
useEffect(() => { apply(theme === "system" ? getSystem() : theme); }, [theme, apply, getSystem]);
useEffect(() => { // Listen for system preference changes
if (theme !== "system") return;
const mq = matchMedia("(prefers-color-scheme: dark)");
const handler = () => apply(getSystem());
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [theme, apply, getSystem]);
const setTheme = useCallback((t: Theme) => {
localStorage.setItem(storageKey, t); setThemeState(t);
}, [storageKey]);
return <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>;
}
Full implementation with toggleTheme, disableTransitionOnChange, and testing patterns in references/theming-architecture.md.
Inline script in <head> runs before paint:
const themeScript = `(function(){
var t=localStorage.getItem('theme')||'system';
var d=t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme',d?'dark':'light');
document.documentElement.style.colorScheme=d?'dark':'light';
})()`;
// In layout.tsx
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body><ThemeProvider>{children}</ThemeProvider></body>
</html>
Layer brand tokens on top of semantic tokens for white-label products:
[data-brand="corporate"] {
--brand-primary: #0066cc;
--brand-primary-hover: #0052a3;
--brand-font-heading: "Helvetica Neue", sans-serif;
--brand-radius: 0.25rem;
}
[data-brand="startup"] {
--brand-primary: #7c3aed;
--brand-primary-hover: #6d28d9;
--brand-font-heading: "Poppins", sans-serif;
--brand-radius: 1rem;
}
/* Map brand tokens into semantic tokens */
:root {
--interactive-primary: var(--brand-primary);
--interactive-primary-hover: var(--brand-primary-hover);
}
Multi-platform token generation from a single JSON source:
// style-dictionary.config.js — generates CSS, iOS Swift, and Android XML
module.exports = {
source: ["tokens/**/*.json"],
platforms: {
css: {
transformGroup: "css", buildPath: "dist/css/",
files: [{ destination: "variables.css", format: "css/variables",
options: { outputReferences: true } }],
},
ios: {
transformGroup: "ios-swift", buildPath: "dist/ios/",
files: [{ destination: "DesignTokens.swift", format: "ios-swift/class.swift",
className: "DesignTokens" }],
},
android: {
transformGroup: "android", buildPath: "dist/android/",
files: [{ destination: "colors.xml", format: "android/colors",
filter: { attributes: { category: "color" } } }],
},
},
};
See references/design-tokens.md for token category definitions, custom transforms, and platform-specific output examples.
@media (prefers-reduced-motion: reduce) {
:root {
--duration-fast: 0ms;
--duration-normal: 0ms;
--duration-slow: 0ms;
}
}
@media (prefers-contrast: high) {
:root {
--text-primary: #000000;
--surface-default: #ffffff;
--border-default: #000000;
--interactive-primary: #0000ee;
}
}
@media (forced-colors: active) {
.button { border: 2px solid currentColor; }
.card { border: 1px solid CanvasText; }
}
Format: [category]-[property]-[variant]-[state] (e.g. color-border-input-focus)
text-primary not textPrimarydanger not red-hover, -focus, -active, -disabledspacing-4, font-size-lgChange management: Propose → Review (design + eng) → Test (all platforms/themes) → Deprecate (with migration path) → Remove (after deprecation period).
{
"color.primary": {
"value": "{color.primitive.blue.500}",
"deprecated": true,
"deprecatedMessage": "Use semantic.accent.default instead",
"replacedBy": "semantic.accent.default"
}
}