DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

Building an Accessible, Inclusive Design System from First Principles

Building an Accessible, Inclusive Design System from First Principles

Building an Accessible, Inclusive Design System from First Principles

A strong design system speeds up development, enforces consistency, and makes products usable for everyone. This guide focuses on practical patterns you can adopt today to design, implement, and maintain an accessible, inclusive design system for frontend apps. You’ll find real code patterns, tests, and step-by-step guidance you can apply to most modern React projects (though the concepts translate to other frameworks).

Why this topic

  • Accessibility and inclusion are foundational, not afterthoughts.
  • A robust design system helps teams ship features faster while meeting diverse user needs.
  • The patterns here emphasize clarity, maintainability, and real-world constraints like audits, team onboarding, and progressive enhancement.

Overview

  • Part 1: Principles and governance
  • Part 2: Token-based design system architecture
  • Part 3: Accessible components and patterns
  • Part 4: Theming and color system for contrast and inclusion
  • Part 5: Responsive and motion considerations
  • Part 6: Testing, auditing, and maintenance
  • Part 7: Example implementation: a small component library
  • Part 8: Deployment, code organization, and collaboration tips

Part 1 - Principles and governance

  • Start with inclusive design goals: high contrast, keyboard focus visibility, screen reader compatibility, sufficient touch targets, and respectful motion.
  • Define a small, sustainable token surface: color, typography, spacing, radii, shadows, breakpoints.
  • Establish a lightweight governance model: a design-system working group, contribution guidelines, contributor license, and a decision log.
  • Build for progressive enhancement: core features work with minimal styling; design tokens augment behavior for richer experiences.

Part 2 - Token-based design system architecture
Tokens are the single source of truth for visuals. They drive consistency and enable easy theming.

  • Core tokens (theme-agnostic)

    • color: primary, secondary, surface, background, text, border
    • typography: fontFamily, fontSizeScale, fontWeight
    • spacing: scale (e.g., 4, 8, 12, 16, 24, 32)
    • radii: small, medium, large
    • shadows: elevation levels
    • breakpoints: sm, md, lg
  • Token file example (TypeScript)

    • src/tokens/index.ts
    • src/tokens/colors.ts
    • src/tokens/typography.ts
    • src/tokens/breakpoints.ts

Code pattern (TypeScript)

  • Define a theme shape

    • interface ThemeTokens { colors: Colors; typography: Typography; spacing: Spacing; radii: Radii; shadows: Shadows; breakpoints: Breakpoints; }
    • export const lightTheme: ThemeTokens = { colors: { text: '#0f1220', background: '#ffffff', surface: '#f7f7f9', border: '#e5e7eb', primary: '#2563eb', secondary: '#9333ea', ... }, typography: { fontFamily: '"Inter", system-ui, Arial', fontSizeScale: , fontWeight: { regular: 400, bold: 700 } }, spacing: { scale: }, radii: { small: 4, medium: 8, large: 12 }, shadows: { 0: 'none', 1: '0 1px 2px rgba(0,0,0,.05)', 2: '0 4px 8px rgba(0,0,0,.08)' }, breakpoints: { sm: '640px', md: '768px', lg: '1024px' } }
  • Theme provider

    • Using a CSS-in-JS library (e.g., styled-components, stitches, or vanilla CSS vars)
    • Example with CSS variables (no library)
    • src/styles/theme.css :root { color-text: #0f1220; color-background: #ffffff; color-surface: #f7f7f9; color-border: #e5e7eb; color-primary: #2563eb; radius-small: 4px; radius-medium: 8px; space-4: 4px; space-8: 8px; space-16: 16px; ... } @media (prefers-color-scheme: dark) { :root { color-background: #0b1020; color-surface: #14172a; color-text: #e8eaf6; color-border: #2a2f52; color-primary: #60a5fa; } }
  • Theming API

    • Create a theme object and pass to a ThemeProvider
    • Example with React context:
    • src/ui/ThemeProvider.tsx import React, { createContext, useContext } from 'react'; const ThemeContext = createContext(lightTheme); export const ThemeProvider: React.FC<{theme?: ThemeTokens}> = ({ children, theme = lightTheme }) => ( {children} ); export const useTheme = () => useContext(ThemeContext);
    • Components read tokens via CSS variables or a CSS-in-JS hook.

Part 3 - Accessible components and patterns

  • Button

    • Accessible label, keyboard focus, aria-pressed for toggle, disabled state
    • Example (React, CSS variables)
    • {children}
    • CSS: .btn { background: var(color-primary); color: white; padding: var(space-8) var(space-16); border: none; border-radius: var(radius-medium); cursor: pointer; font: inherit; } .btn:focus-visible { outline: 3px solid color-mm; outline-offset: 2px; } .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
  • Text input

    • Labels must be associated via htmlFor and id
    • Use aria-invalid and aria-describedby for error messages
    • Example: Email {hasError && Please enter a valid email.}
  • Accessible patterns

    • Focus management: when opening a modal, return focus to trigger after close
    • Keyboard navigation: trap focus inside modal
    • Form validation: real-time feedback with screen-reader-friendly messages
    • Color contrast: ensure text color contrast ratio >= 4.5:1 for body text; use a contrast-check tool during design

Code pattern - accessible modal

  • Implementation idea (React)
    • Use a focus trap utility or implement simple trap:
    • onMount: save last focused element, set focus to modal root, add keydown listener for Tab/Shift+Tab
    • onClose: restore previous focus
    • Provide aria-modal="true" and role="dialog"
    • Ensure header (accessible name) via aria-labelledby and description via aria-describedby

Part 4 - Theming and color system for contrast and inclusion

  • Color choices
    • Primary color should be distinguishable on light and dark surfaces
    • Ensure at least one of primary/secondary has high enough contrast against surface
  • Contrast checks
    • Use a tool or library to verify ratios; integrate into design tokens
    • Example: function getContrast(hex1, hex2) returns WCAG ratio; warn if below 4.5:1 for body text
  • Motion preferences
    • Respect users who prefer-reduced-motion
    • In CSS: @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }

Code pattern - color tokens with contrast evaluator

  • A small utility to verify contrast during build
    • src/utils/contrast.ts export function luminance(hex: string) { // convert hex to luminance // implementation } export function contrastRatio(hex1: string, hex2: string) { // compute ratio // ... }
  • Integrate with a prebuild check to fail if critical tokens fail.

Part 5 - Responsive and motion considerations

  • Breakpoints and responsive tokens
    • Use a scalable spacing system and fluid typography
    • Implement a responsive utility:
    • In CSS: use min() and clamp() for fluid typography and spacing
  • Motion
    • Prefer subtle motion; avoid parallax that reduces readability
    • Provide reduced-motion fallback
    • Example CSS: .card { transition: transform 0.2s ease; } @media (prefers-reduced-motion: reduce) { .card { transition: none; } }

Part 6 - Testing, auditing, and maintenance

  • Accessibility testing
    • Automated: aXe/ARIA audits, Lighthouse accessibility score
    • Manual: keyboard navigation, screen reader checks (NVDA, VoiceOver)
  • Visual regression
    • Snapshot tests for components with tokens; ensure theme changes update visuals
  • Documentation and onboarding
    • A living component catalog with usage examples and accessible notes
  • Governance and change management
    • Use a changelog for design tokens; deprecate tokens gracefully

Part 7 - Example implementation: a small component library

  • Project structure (example)

    • src/
    • components/
      • Button/
      • Button.tsx
      • Button.css
      • Button.stories.tsx
      • Button.test.tsx
      • Input/
      • Input.tsx
      • Input.css
      • Input.stories.tsx
      • Input.test.tsx
      • Modal/
      • Modal.tsx
      • Modal.css
      • Modal.stories.tsx
      • Modal.test.tsx
    • tokens/
      • index.ts
      • colors.ts
      • typography.ts
      • spacing.ts
      • radii.ts
      • shadows.ts
      • breakpoints.ts
    • ui/
      • ThemeProvider.tsx
      • useTheme.ts
    • utils/
      • contrast.ts
    • styles/
      • theme.css
    • package.json
  • Button.tsx (simplified)

    import React from 'react';

    type ButtonProps = React.ButtonHTMLAttributes & { variant?: 'primary' | 'secondary'; label?: string };

    export const Button: React.FC = ({ variant = 'primary', children, ...rest }) => {

    const className = btn btn${variant};

    return (



    {children}



    );

    };

  • Button.css (uses CSS variables)

    .btn { background: var(color-primary); color: white; padding: var(space-8) var(space-16); border: none; border-radius: var(radius-medium); cursor: pointer; font-family: var(font-family); }

    .btnsecondary { background: var(color-secondary); }

    .btn:focus-visible { outline: 3px solid color-mix(in oklab, var(color-primary), white 20%); outline-offset: 2px; }

  • Input.tsx
    import React from 'react';
    type InputProps = React.InputHTMLAttributes & { label: string; id: string; error?: string | null };
    export const Input: React.FC = ({ id, label, error, ...rest }) => (

    {label}

    {error && {error}}

    );

  • Modal.tsx (simple focus management)

    import React, { useEffect, useRef } from 'react';

    type ModalProps = { open: boolean; onClose: () => void; title: string; children: React.ReactNode };

    export const Modal: React.FC = ({ open, onClose, title, children }) => {

    const modalRef = useRef(null);

    useEffect(() => {

    if (!open) return;

    const previouslyFocused = document.activeElement as HTMLElement;

    modalRef.current?.focus();

    const onKey = (e: KeyboardEvent) => {

    if (e.key === 'Escape') onClose();

    };

    document.addEventListener('keydown', onKey);

    return () => {

    document.removeEventListener('keydown', onKey);

    previouslyFocused?.focus();

    };

    }, [open, onClose]);

    if (!open) return null;

    return (





    {children} ); };
  • Stories and tests

    • Provide Storybook stories for components; tests ensure accessibility attributes exist and color tokens apply
    • Example: Button story with aria-label focused states

Part 8 - Deployment, code organization, and collaboration tips

  • Versioning tokens
    • Use semantic versioning for tokens; patch/minor/major changes
  • Documentation site
    • Host a design-system site (e.g., Docusaurus, Next.js) that shows tokens, usage, and accessibility notes
  • CI checks
    • Linting, type checks, automated accessibility tests (axe-core), and visual regression checks
  • Collaboration
    • Require design review for token changes
    • Use a central glossary for accessibility terms and color usage

Practical checklist to get started

  • Define your token surface: decide on colors, typography, spacing scales, radii, shadows, and breakpoints.
  • Create a ThemeProvider and a minimal token file set to enable consistent styling across components.
  • Build a small component library focusing on accessibility: Button, Input, and Modal as first-pass components.
  • Add aria attributes, keyboard accessibility, and motion preferences early.
  • Set up automated accessibility tests and visual regression tests.
  • Document usage with examples and accessibility notes; make onboarding easy for new contributors.
  • Plan a lightweight governance process to manage changes and deprecations.

Illustration - how tokens drive design consistency
Imagine you have three UI elements: a primary button, a card, and a form label. If you hard-code colors and spacing in each, changing the brand color later becomes a chore. With tokens:

  • The primary button reads color-primary and space-8/space-16 from tokens.
  • The card uses color-surface, color-border, and spacing tokens.
  • The form label uses typography and color tokens. Change the brand color by updating one token file; all components instantly reflect the update, maintaining consistent typography, spacing, and contrast throughout.

Would you like me to tailor this tutorial to a specific stack (e.g., React with Styled Components, Vue with CSS Modules, or a vanilla JS setup), or to align with your current project’s accessibility requirements and design system maturity?

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)