DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

Building a Playful Yet Practical Design System: Real-World Patterns for Accessible UI Tokens

Building a Playful Yet Practical Design System: Real-World Patterns for Accessible UI Tokens

Building a Playful Yet Practical Design System: Real-World Patterns for Accessible UI Tokens

Design systems often feel like a Styling Parade-tons of tokens, patterns, and glossaries, but real teams struggle to translate them into fast, accessible, and maintainable frontend code. This tutorial shows you how to design and implement a practical design system from the ground up with real code patterns you can drop into a project today. We’ll cover token strategy, accessible components, theming, and sustainable collaboration workflows.

1. Define a pragmatic token taxonomy

A design system is only as good as its tokens. Start with a minimal, scalable taxonomy you can grow without friction.

  • Color tokens: semantic names (primary, surface, on-primary) rather than raw hex values
  • Typography tokens: font families, sizes, line-heights, letter-spacings
  • Spacing tokens: a small scale (e.g., 4, 8, 12, 16) that maps to consistent margins/paddings
  • Border tokens: radii, widths, and shadows that reflect elevation
  • Iconography and elevation: references to components and states rather than pixel darts

Example token file (JSON) you can feed into a CSS-in-JS or CSS variables pipeline:

{
"color": {
"primary": "#2563EB",
"onPrimary": "#FFFFFF",
"surface": "#F3F4F6",
"onSurface": "#111827",
"muted": "#6B7280",
"error": "#EF4444"
},
"typography": {
"fontFamily": "Inter, system-ui, -apple-system, Arial",
"baseSize": 16,
"lineHeight": 1.5,
"weightRegular": 400,
"weightMedium": 500,
"weightBold": 700
},
"spacing": {
"xs": 4,
"sm": 8,
"md": 12,
"lg": 16,
"xl": 24
},
"radii": {
"sm": 4,
"md": 8,
"lg": 12,
"circle": 9999
},
"shadow": {
"surface": "0 1px 2px rgba(0,0,0,.08)",
"Elevated": "0 6px 20px rgba(0,0,0,.15)"
}
}

  • Keep tokens in a single source of truth.
  • Use semantic naming so UI code expresses intent, not values.
  • Provide a mapping layer so design tokens can be swapped for dark mode or themes without touching component code. ### 2. Implement a robust theming approach

A practical design system supports light/dark or multiple themes, with minimal code churn.

  • Theme objects: export a theme per variant (light, dark) and a manager that swaps at runtime.
  • Centralized provider: React context to supply tokens to all components.
  • CSS-in-JS or CSS variables: choose one approach and stay consistent.

Example using CSS variables (vanilla-friendly):

/* tokens.css */
:root {
color-primary: #2563EB;
color-on-primary: #FFFFFF;
color-surface: #F3F4F6;
font-family: Inter, system-ui, -apple-system, Arial;
base-size: 16px;
radius-md: 8px;
shadow-surface: 0 1px 2px rgba(0,0,0,.08);
}
[data-theme="dark"] {
color-primary: #3B82F6;
color-on-primary: #0F172A;
color-surface: #111827;
shadow-surface: 0 6px 20px rgba(0,0,0,.6);
}
.btn {
background: var(color-primary);
color: var(color-on-primary);
padding: 0.5em 1em;
border-radius: var(radius-md);
box-shadow: var(shadow-surface);
border: none;
font-family: var(font-family);
font-size: calc(var(base-size) * 0.95);
}

Toggle example in React:

import React, { useState } from 'react';
import './tokens.css';

export function ThemeToggle() {
const [theme, setTheme] = useState('light');
React.useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);

return (
setTheme(t => (t === 'light' ? 'dark' : 'light'))}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Theme

);
}

  • Pros: quick to statically analyze, good for SSR.
  • Cons: can lead to duplication if overused; plan for a token-driven CSS pipeline if your project grows. ### 3. Build accessible, token-driven components

Accessibility (a11y) should be baked in from day one. Create components that adhere to semantic HTML, keyboard navigability, and color contrast guidelines.

  • Use proper semantic elements: nav, header, main, footer, button, etc.
  • Ensure color contrast passes WCAG AA for text on surfaces (contrast checker helps).
  • Expose ARIA attributes where needed, but prefer native semantics.
  • Provide visual focus indicators that are consistent with tokens.

Example: a Button component in React with tokens

import React from 'react';
import './tokens.css';

export function Button({ children, onClick, variant = 'primary', size = 'md' }) {
const className = btn btn-${variant} btn-${size};
return (

{children}

);
}

CSS tokens for variants and sizes:

.btn-primary { background: var(color-primary); color: var(color-on-primary); }
.btn-secondary { background: transparent; color: var(color-primary); border: 1px solid var(color-primary); }
.btn-md { padding: 0.5em 1em; font-size: 1rem; border-radius: var(radius-md); }

  • Accessible defaults: readable font sizes, focus rings, and high-contrast states.
  • Testing: use axe-core or a11y testing tools in CI. ### 4. Create scalable component primitives

Break UI into small, composable primitives: Box, Text, Icon, and then compose up to complex components.

  • Box: a generic layout primitive with flex/grid props
  • Text: typography with variants
  • Icon: scalable SVG rendering with scalable props
  • Avatar, Card, Input, Select: built from primitives

Tiny Box primitive example (React):

export function Box({ as: Element = 'div', children, ...props }) {
return ;
}

export function Text({ as = 'p', children, variant = 'body', ...props }) {
const Tag = as;
const className = text text-${variant};
return {children};
}

  • This keeps styling predictable and encourages consistent spacing and typography across components. ### 5. Adopt a pragmatic development workflow

A design system should be easy to evolve without slowing product work.

  • Source of truth: keep tokens and theme definitions in a dedicated repo or package.
  • Versioning: semantic versioning for the design system package; publish minor updates for new tokens, patch for fixes.
  • Storybook or component explorer: document usage, props, and accessibility notes.
  • Design-token pipeline: build step to generate CSS variables or theme JSON from a single source of truth.

Example npm workflow (high level):

  • design-tokens.json as source
  • A small script converts tokens.json to CSS variables and a TS interface
  • package exports:
    • tokens.css
    • theme.ts (for React usage)
    • components/ (shared components)
  • CI runs tests and linting on PRs
  • When a change adds a token or a breaking change, publish a new minor version with changelog

    6. Real-world integration patterns

  • Consume tokens in both CSS and JS: export tokens to CSS variables and to a JS theme object for runtime usage.

  • Theming in apps with SSR: ensure theme is rendered on the server to avoid FOUC (flash of unstyled content).

  • Typography ramps: provide line-height and size scales that slide gracefully to responsive breakpoints.

  • Component variants: keep a minimal set of props for switchable behavior; avoid token-splitting logic inside components.

Code pattern: consuming tokens in a React component with a theme prop

import React from 'react';
import { Button } from './components/Button';
import { useTheme } from './theme-context';

export function Header() {
const { theme } = useTheme();
return (

Brand


Get Started
{theme}

);
}
  • The ThemeContext provides a clean way to swap themes at runtime without reloading components.
  • Keep a tight feedback loop for designers: integrate with design tooling to export tokens automatically from design files.

    7. Testing strategy for a design system

  • Visual tests: use Percy or Chromatic to catch UI regressions.

  • Token validation: unit tests to ensure token values map correctly to computed styles.

  • Accessibility checks: integrate axe-core in CI; include a11y tests for key components.

  • Failing fast: add linting that enforces token usage over hard-coded values.

Example test snippet (Jest + Testing Library):

import React from 'react';
import { render, screen } from '@testing-library/react';
import { Button } from './components/Button';

test('button uses primary token colors', () => {
render(Click me);
const btn = screen.getByText('Click me');
expect(btn).toHaveStyle(background: #2563EB);
expect(btn).toHaveStyle(color: #FFFFFF);
});

8. Example project structure

  • src/
    • tokens/
    • tokens.json
    • tokens.css (build output)
    • theme/
    • ThemeProvider.tsx
    • components/
    • Button.tsx
    • Text.tsx
    • Box.tsx
    • Icon.tsx
    • Avatar.tsx
    • hooks/
    • useTheme.ts
    • pages/
    • index.tsx
    • tests/
    • components/
      • Button.test.tsx
  • .storybook/ (optional)
  • CI configuration (lint, test, build)
  • README with quick-start and contribution guide

    9. Quick-start guide

1) Set up token source:

  • Create tokens.json with color, typography, spacing, radii, and shadow. 2) Build tokens into CSS and TS:
  • Run a small script to generate tokens.css and a theme.tsx for React usage. 3) Implement a Button using tokens:
  • Use token-driven CSS classes or inline styles pulled from the theme object. 4) Add a light/dark theme toggle:
  • Swap data-theme attribute on the root element and re-run styles. 5) Document usage:
  • Publish the design system package and maintain a Storybook page for components.

    10. Practical tips from field experience

  • Start small: implement a single component (Button) with tokens, then scale.

  • Favor semantic tokens over hard-coded values to future-proof your UI.

  • Keep design tokens accessible: provide a clear naming scheme and a public changelog.

  • Align with design and product teams early; token changes can ripple through the UI-plan deprecation paths and communication.

  • Automate as much as possible: token-to-theme generation, automated visual tests, and CI checks.

Illustration: Think of the design system as a garden. Tokens are the soil and seeds; components are the plants that grow from them. If the soil is rich and consistent, the plants flourish with less effort and more reliability.
If you’d like, I can tailor this into a runnable starter repo (TypeScript + React) with a minimal build script, or adapt the patterns to a particular framework (e.g., vanilla JS + CSS, Vue, or Svelte). Would you prefer a full repository scaffold or a focused starter kit for a specific technology stack?

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)