DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

Building a Accessible, Performant Design System: From Tokens to Tooling

Building a Accessible, Performant Design System: From Tokens to Tooling

Building a Accessible, Performant Design System: From Tokens to Tooling

Design systems are more than a collection of components; they are the collaborative fabric that keeps a product cohesive as it grows. This tutorial walks you through building a frontend design system that is accessible, performant, and developer-friendly. You’ll move from design tokens to a living component library with practical patterns, code examples, and actionable steps you can apply today.

1) Define a stable design language

A design system starts with a shared language. Define these core pieces once and reuse them across teams.

  • Color tokens: primary, secondary, surface, on-surface, error, success, disabled
  • Typographic scale: font-family, font-size steps, line-height, letter-spacing
  • Spacing system: a modular scale (e.g., 4px, 8px, 12px, 16px, 24px, 32px)
  • Elevation/shadows: levels for surfaces
  • Radius and borders: radii for components, border widths
  • Interaction states: hover, focus, active, disabled

Example token file (JSON) you can import in JS/TS apps:

{
  "color": {
    "primary": "#2563EB",
    "onPrimary": "#FFFFFF",
    "surface": "#FFFFFF",
    "onSurface": "#1F2937",
    "secondary": "#06B6D4",
    "onSecondary": "#001F2C",
    "error": "#DC2626",
    "success": "#16A34A",
    "disabled": "#D1D5DB",
    "background": "#F7F7FB"
  },
  " typography": {
    "fontFamily": "\"Inter\", system-ui, -apple-system, Segoe UI, Roboto, Arial",
    "sizes": {
      "xs": 12,
      "sm": 14,
      "md": 16,
      "lg": 20,
      "xl": 24
    },
    "weights": {
      "regular": 400,
      "medium": 500,
      "bold": 700
    },
    "lineHeights": {
      "tight": 1.15,
      "normal": 1.5
    }
  },
  "space": ,
  "radii": {
    "sm": 4,
    "md": 8,
    "lg": 12,
    "full": 9999
  },
  "shadow": {
    "elevation-1": "0 1px 2px rgba(0,0,0,.08)",
    "elevation-2": "0 6px 12px rgba(0,0,0,.08)"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Establish a single source of truth: host tokens in a central place (e.g., design-tokens repo or package).
  • Use semantic naming: instead of color names like red-500, prefer color.primary, color.surface, etc.
  • Version tokens: treat tokens as code; bump versions when design decisions change. ### 2) Build a token-driven CSS system

There are two popular paths: CSS-in-JS or CSS custom properties (variables) with a build step. Choose what fits your stack. Here are both.

Path A: CSS Variables (vanilla-friendly)

  • Create a root set of CSS variables that map to tokens.
  • Use a small utility to generate themes if you need dark mode.

Example (tokens.css):

:root {
  color-primary: #2563EB;
  color-on-primary: #FFFFFF;
  color-surface: #FFFFFF;
  color-on-surface: #1F2937;
  space-0: 0px;
  space-1: 4px;
  space-2: 8px;
  space-3: 12px;
  space-4: 16px;
  radius-md: 8px;
  shadow-elevation-1: 0 1px 2px rgba(0,0,0,.08);
}
Enter fullscreen mode Exit fullscreen mode
  • Use a SASS/LESS or a build step to output these values.
  • For dark mode, define a [data-theme="dark"] section and override variables.

Path B: CSS-in-JS (styled-components / stitches / vanillaflow)

  • Create a theme object that mirrors tokens.
  • Provide a ThemeProvider to wrap your app.
  • Use tokens directly in components for consistent styling.

Example with a minimal style system (Plain JS):

// theme.js
export const theme = {
  color: {
    primary: '#2563EB',
    onPrimary: '#FFFFFF',
    surface: '#FFFFFF',
    onSurface: '#1F2937',
    background: '#F7F7FB',
    disabled: '#D1D5DB'
  },
  space: ,
  radii: { md: 8, lg: 12, full: 9999 },
  fontSizes: { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 }
}
Enter fullscreen mode Exit fullscreen mode
// App.jsx
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';

function App({children}) {
  return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
Enter fullscreen mode Exit fullscreen mode
  • Create small utility functions to convert token scales into CSS values when needed. ### 3) Create a modular component library

Focus on two goals: accessibility and composability.

  • Accessibility: ensure components are keyboard navigable and screen-reader friendly. Use proper roles, aria-labels, and semantic HTML.
  • Composability: design small primitives that can be combined into complex components.

Core primitives:

  • Box: a simple div with padding/margin and responsive props
  • Text: typographic component with variants
  • Button: accessible, with different variants (primary, secondary)
  • Input, Select, Textarea: accessible by default with proper labels
  • Card, Modal, Tooltip: composed from Box/Text primitives

Example Box component (React + TypeScript):

type BoxProps = React.HTMLAttributes<HTMLDivElement> & {
  as?: keyof JSX.IntrinsicElements;
  p?: number;
  m?: number;
  bg?: string;
  radius?: string | number;
  className?: string;
  style?: React.CSSProperties;
}

export const Box = ({ as: Component = 'div', p, m, bg, radius, className, ...rest }: BoxProps) => {
  const space = (n?: number) => (n != null ? { padding: `${n * 4}px` } : {});
  const margin = (n?: number) => (n != null ? { margin: `${n * 4}px` } : {});
  return (
    <Component
      className={className}
      style={{
        background: bg,
        borderRadius: radius,
        ...space(p),
        ...margin(m),
        ...rest.style
      }}
      {...rest}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Example PrimaryButton:

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary';
  wide?: boolean;
};

export const Button = ({ variant = 'primary', wide, children, ...rest }: ButtonProps) => {
  const common = {
    padding: '12px 16px',
    border: 'none',
    borderRadius: 8,
    cursor: 'pointer',
    fontWeight: 600
  } as React.CSSProperties;

  const styles =
    variant === 'primary'
      ? {
          backgroundColor: '#2563EB',
          color: '#FFFFFF',
          ...common
        }
      : {
          backgroundColor: 'transparent',
          color: '#2563EB',
          border: '1px solid #2563EB',
          ...common
        };

  return (
    <button style={styles} aria-label={typeof children === 'string' ? children : 'button'} {...rest}>
      {children}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Accessibility tip: ensure focus styles (outline or box-shadow) are visible and consistent.

4) Implement design tokens in real-world UI

  • Use tokens for all color, spacing, and typography decisions across components.
  • Build a small helper to map token names to CSS values in your chosen styling approach.

Example usage in a card:

import { Box } from './primitives/Box';
import { Text } from './primitives/Text';
import { Button } from './primitives/Button';

export function Card({ title, content }) {
  return (
    <Box bg="surface" p={4} radius="md" style={{ boxShadow: 'var(shadow-elevation-1)' }}>
      <Text variant="h6" as="h3" mb={2}>{title}</Text>
      <Text variant="body" as="p" mb={3}>{content}</Text>
      <Button>Learn more</Button>
    </Box>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: The example assumes your Text component supports variants like h6 and body that map to token font sizes and weights.

5) Add a theming mechanism (dark mode)

Dark mode should be a first-class theme variant, not an afterthought.

  • Create a second theme object that mirrors the light theme with inverted luminance.
  • Use a data-theme attribute or a ThemeProvider toggle to switch themes.
  • Persist the user choice (localStorage) and respect OS-level preferences.

Example toggle hook (React):

import { useEffect, useState } from 'react';
import { theme as light } from './theme-light';
import { theme as dark } from './theme-dark';
import { ThemeProvider } from 'styled-components';

function usePrefersDark() {
  const mq = typeof window !== 'undefined' ? window.matchMedia('(prefers-color-scheme: dark)') : null;
  const [prefersDark, setPrefersDark] = useState(mq?.matches ?? false);
  useEffect(() => {
    if (!mq) return;
    const handler = (e: MediaQueryListEvent) => setPrefersDark(e.matches);
    mq.addEventListener?.('change', handler);
    return () => mq.removeEventListener?.('change', handler);
  }, [mq]);
  return prefersDark;
}

export function AppShell({ children }) {
  const [useDark, setUseDark] = useState(false);
  const prefersDark = usePrefersDark();
  useEffect(() => {
    const stored = localStorage.getItem('theme');
    if (stored) setUseDark(stored === 'dark');
    else setUseDark(prefersDark);
  }, [prefersDark]);

  useEffect(() => {
    localStorage.setItem('theme', useDark ? 'dark' : 'light');
  }, [useDark]);

  const currentTheme = useDark ? dark : light;

  return (
    <ThemeProvider theme={currentTheme}>
      {children}
      {/* a simple toggle could call setUseDark(!useDark) */}
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

6) Documentation and discoverability

  • Document tokens, components, and usage patterns in a single source-of-truth site.
  • Create a living style guide, auto-generated from code where possible.
  • Use story-like examples: show a Button in different states, a Text block, a Card, and a Modal.

Example of a minimal style guide page structure:

  • Tokens: color, typography, spacing, radii
  • Primitives: Box, Text, Icon
  • Components: Button, Input, Card, Modal
  • Accessibility: focus management, keyboard shortcuts
  • Theming: theme switch and OS-level preference

    7) Performance considerations

  • Token-driven styling helps enable tree-shaking and smaller bundle sizes.

  • Use CSS containment or virtualization for long lists to avoid unnecessary paints.

  • Prefer simple, deterministic CSS to minimize layout thrash (avoid reflow-heavy patterns).

  • Lazy-load components where appropriate and memoize heavy UI parts.

Practical patterns:

  • Use CSS variables for theme changes to avoid re-rendering large component trees.
  • Component-level memoization for pure UI components.
  • Debounce expensive layout calculations and use requestAnimationFrame for visual updates.

    8) Testing the design system

  • Visual regression tests: snapshot components in various themes and states.

  • Accessibility testing: keyboard navigation tests, ARIA labeling checks.

  • Unit tests for tokens: verify that the token map resolves to expected CSS values.

  • End-to-end: confirm critical flows (e.g., opening a modal, submitting a form) render correctly across themes.

Example simple unit test (Jest) for token resolution:

import { resolveColor } from './tokenUtils';

test('resolveColor should map token names to hex values', () => {
  const color = resolveColor('primary');
  expect(color).toBe('#2563EB');
});
Enter fullscreen mode Exit fullscreen mode

9) Deployment and versioning

  • Publish the tokens and primitives as a package (e.g., design-tokens, ui-primitives).
  • Version using semantic versioning; major changes should update the token surface or API of primitives.
  • Include a changelog and migration notes when breaking changes occur.
  • Integrate with CI to run tests, visual diffs, and linting on PRs.

    10) A practical roadmap to start now

  • Week 1: Agree on tokens and set up a token repo. Create a light CSS variable system or a small theming setup. Build Box/Text primitives and a Button primitive.

  • Week 2: Implement a small Card and Modal to demonstrate composition. Add accessibility tests.

  • Week 3: Create a simple style guide site that documents tokens and components. Start dark mode theming.

  • Week 4: Integrate with a CI workflow, add visual regression tests, and publish packages.

  • Ongoing: Enforce design-system usage with lint rules, code reviews, and internal docs.

Illustration: Imagine the design system as the ergonomic skeleton of your UI. Tokens are the bones (stable, reusable), primitives are the muscles (flexible, movable), and components are the body (visible, interactive). The better the tokens, the more fluid the UI, and the easier it is for developers and designers to collaborate without stepping on each other’s toes.

If you’d like, I can tailor this to your stack (React, Vue, Svelte, or plain HTML/CSS) and provide a ready-to-run starter repo with a minimal component library and tokens. Would you prefer a React-focused starter or a framework-agnostic approach?

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)