Design System Patterns
Master design system architecture to create consistent, maintainable, and scalable UI foundations across web and mobile applications.
When to Use This Skill
- Creating design tokens for colors, typography, spacing, and shadows
- Implementing light/dark theme switching with CSS custom properties
- Building multi-brand theming systems
- Architecting component libraries with consistent APIs
- Establishing design-to-code workflows with Figma tokens
- Creating semantic token hierarchies (primitive, semantic, component)
- Setting up design system documentation and guidelines
Core Capabilities
1. Design Tokens
- Primitive tokens (raw values: colors, sizes, fonts)
- Semantic tokens (contextual meaning: text-primary, surface-elevated)
- Component tokens (specific usage: button-bg, card-border)
- Token naming conventions and organization
- Multi-platform token generation (CSS, iOS, Android)
2. Theming Infrastructure
- CSS custom properties architecture
- Theme context providers in React
- Dynamic theme switching
- System preference detection (prefers-color-scheme)
- Persistent theme storage
- Reduced motion and high contrast modes
3. Component Architecture
- Compound component patterns
- Polymorphic components (as prop)
- Variant and size systems
- Slot-based composition
- Headless UI patterns
- Style props and responsive variants
4. Token Pipeline
- Figma to code synchronization
- Style Dictionary configuration
- Token transformation and formatting
- CI/CD integration for token updates
Quick Start
typescript
1// Design tokens with CSS custom properties
2const tokens = {
3 colors: {
4 // Primitive tokens
5 gray: {
6 50: "#fafafa",
7 100: "#f5f5f5",
8 900: "#171717",
9 },
10 blue: {
11 500: "#3b82f6",
12 600: "#2563eb",
13 },
14 },
15 // Semantic tokens (reference primitives)
16 semantic: {
17 light: {
18 "text-primary": "var(--color-gray-900)",
19 "text-secondary": "var(--color-gray-600)",
20 "surface-default": "var(--color-white)",
21 "surface-elevated": "var(--color-gray-50)",
22 "border-default": "var(--color-gray-200)",
23 "interactive-primary": "var(--color-blue-500)",
24 },
25 dark: {
26 "text-primary": "var(--color-gray-50)",
27 "text-secondary": "var(--color-gray-400)",
28 "surface-default": "var(--color-gray-900)",
29 "surface-elevated": "var(--color-gray-800)",
30 "border-default": "var(--color-gray-700)",
31 "interactive-primary": "var(--color-blue-400)",
32 },
33 },
34};
Key Patterns
Pattern 1: Token Hierarchy
css
1/* Layer 1: Primitive tokens (raw values) */
2:root {
3 --color-blue-500: #3b82f6;
4 --color-blue-600: #2563eb;
5 --color-gray-50: #fafafa;
6 --color-gray-900: #171717;
7
8 --space-1: 0.25rem;
9 --space-2: 0.5rem;
10 --space-4: 1rem;
11
12 --font-size-sm: 0.875rem;
13 --font-size-base: 1rem;
14 --font-size-lg: 1.125rem;
15
16 --radius-sm: 0.25rem;
17 --radius-md: 0.5rem;
18 --radius-lg: 1rem;
19}
20
21/* Layer 2: Semantic tokens (meaning) */
22:root {
23 --text-primary: var(--color-gray-900);
24 --text-secondary: var(--color-gray-600);
25 --surface-default: white;
26 --interactive-primary: var(--color-blue-500);
27 --interactive-primary-hover: var(--color-blue-600);
28}
29
30/* Layer 3: Component tokens (specific usage) */
31:root {
32 --button-bg: var(--interactive-primary);
33 --button-bg-hover: var(--interactive-primary-hover);
34 --button-text: white;
35 --button-radius: var(--radius-md);
36 --button-padding-x: var(--space-4);
37 --button-padding-y: var(--space-2);
38}
Pattern 2: Theme Switching with React
tsx
1import { createContext, useContext, useEffect, useState } from "react";
2
3type Theme = "light" | "dark" | "system";
4
5interface ThemeContextValue {
6 theme: Theme;
7 resolvedTheme: "light" | "dark";
8 setTheme: (theme: Theme) => void;
9}
10
11const ThemeContext = createContext<ThemeContextValue | null>(null);
12
13export function ThemeProvider({ children }: { children: React.ReactNode }) {
14 const [theme, setTheme] = useState<Theme>(() => {
15 if (typeof window !== "undefined") {
16 return (localStorage.getItem("theme") as Theme) || "system";
17 }
18 return "system";
19 });
20
21 const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
22
23 useEffect(() => {
24 const root = document.documentElement;
25
26 const applyTheme = (isDark: boolean) => {
27 root.classList.remove("light", "dark");
28 root.classList.add(isDark ? "dark" : "light");
29 setResolvedTheme(isDark ? "dark" : "light");
30 };
31
32 if (theme === "system") {
33 const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
34 applyTheme(mediaQuery.matches);
35
36 const handler = (e: MediaQueryListEvent) => applyTheme(e.matches);
37 mediaQuery.addEventListener("change", handler);
38 return () => mediaQuery.removeEventListener("change", handler);
39 } else {
40 applyTheme(theme === "dark");
41 }
42 }, [theme]);
43
44 useEffect(() => {
45 localStorage.setItem("theme", theme);
46 }, [theme]);
47
48 return (
49 <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
50 {children}
51 </ThemeContext.Provider>
52 );
53}
54
55export const useTheme = () => {
56 const context = useContext(ThemeContext);
57 if (!context) throw new Error("useTheme must be used within ThemeProvider");
58 return context;
59};
Pattern 3: Variant System with CVA
tsx
1import { cva, type VariantProps } from "class-variance-authority";
2import { cn } from "@/lib/utils";
3
4const buttonVariants = cva(
5 // Base styles
6 "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
7 {
8 variants: {
9 variant: {
10 default: "bg-primary text-primary-foreground hover:bg-primary/90",
11 destructive:
12 "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13 outline:
14 "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15 secondary:
16 "bg-secondary text-secondary-foreground hover:bg-secondary/80",
17 ghost: "hover:bg-accent hover:text-accent-foreground",
18 link: "text-primary underline-offset-4 hover:underline",
19 },
20 size: {
21 sm: "h-9 px-3 text-sm",
22 md: "h-10 px-4 text-sm",
23 lg: "h-11 px-8 text-base",
24 icon: "h-10 w-10",
25 },
26 },
27 defaultVariants: {
28 variant: "default",
29 size: "md",
30 },
31 },
32);
33
34interface ButtonProps
35 extends
36 React.ButtonHTMLAttributes<HTMLButtonElement>,
37 VariantProps<typeof buttonVariants> {
38 asChild?: boolean;
39}
40
41export function Button({ className, variant, size, ...props }: ButtonProps) {
42 return (
43 <button
44 className={cn(buttonVariants({ variant, size, className }))}
45 {...props}
46 />
47 );
48}
Pattern 4: Style Dictionary Configuration
javascript
1// style-dictionary.config.js
2module.exports = {
3 source: ["tokens/**/*.json"],
4 platforms: {
5 css: {
6 transformGroup: "css",
7 buildPath: "dist/css/",
8 files: [
9 {
10 destination: "variables.css",
11 format: "css/variables",
12 options: {
13 outputReferences: true, // Preserve token references
14 },
15 },
16 ],
17 },
18 scss: {
19 transformGroup: "scss",
20 buildPath: "dist/scss/",
21 files: [
22 {
23 destination: "_variables.scss",
24 format: "scss/variables",
25 },
26 ],
27 },
28 ios: {
29 transformGroup: "ios-swift",
30 buildPath: "dist/ios/",
31 files: [
32 {
33 destination: "DesignTokens.swift",
34 format: "ios-swift/class.swift",
35 className: "DesignTokens",
36 },
37 ],
38 },
39 android: {
40 transformGroup: "android",
41 buildPath: "dist/android/",
42 files: [
43 {
44 destination: "colors.xml",
45 format: "android/colors",
46 filter: { attributes: { category: "color" } },
47 },
48 ],
49 },
50 },
51};
Best Practices
- Name Tokens by Purpose: Use semantic names (text-primary) not visual descriptions (dark-gray)
- Maintain Token Hierarchy: Primitives > Semantic > Component tokens
- Document Token Usage: Include usage guidelines with token definitions
- Version Tokens: Treat token changes as API changes with semver
- Test Theme Combinations: Verify all themes work with all components
- Automate Token Pipeline: CI/CD for Figma-to-code synchronization
- Provide Migration Paths: Deprecate tokens gradually with clear alternatives
Common Issues
- Token Sprawl: Too many tokens without clear hierarchy
- Inconsistent Naming: Mixed conventions (camelCase vs kebab-case)
- Missing Dark Mode: Tokens that don't adapt to theme changes
- Hardcoded Values: Using raw values instead of tokens
- Circular References: Tokens referencing each other in loops
- Platform Gaps: Tokens missing for some platforms (web but not mobile)
Resources