Skip to content

Design System with Tokens

Building a comprehensive design system using typestyles tokens

A design system is more than just components—it's a complete set of standards for design and code. This guide shows how to build a token-based design system with typestyles.

Reference implementation: @examples/design-system (tokens + recipes) and @examples/react-design-system (React components). The docs site kitchen sink renders the same recipes.

Token architecture

Three-layer token system

Layer 1: Primitives (raw values)
    ↓
Layer 2: Semantics (meaningful tokens)
    ↓
Layer 3: Components (component-specific)

Layer 1: Primitives

Raw values from your brand guidelines:

ts
// tokens/primitives/colors.ts
export const primitiveColors = {
  // Brand palette
  brand: {
    50: '#eff6ff',
    100: '#dbeafe',
    200: '#bfdbfe',
    300: '#93c5fd',
    400: '#60a5fa',
    500: '#3b82f6',
    600: '#2563eb',
    700: '#1d4ed8',
    800: '#1e40af',
    900: '#1e3a8a',
  },

  // Neutral palette
  gray: {
    50: '#f9fafb',
    100: '#f3f4f6',
    200: '#e5e7eb',
    300: '#d1d5db',
    400: '#9ca3af',
    500: '#6b7280',
    600: '#4b5563',
    700: '#374151',
    800: '#1f2937',
    900: '#111827',
    950: '#030712',
  },

  // Functional colors
  red: { 50: '#fef2f2', 500: '#ef4444', 900: '#7f1d1d' },
  orange: { 50: '#fff7ed', 500: '#f97316', 900: '#7c2d12' },
  yellow: { 50: '#fefce8', 500: '#eab308', 900: '#713f12' },
  green: { 50: '#f0fdf4', 500: '#22c55e', 900: '#14532d' },
  blue: { 50: '#eff6ff', 500: '#3b82f6', 900: '#1e3a8a' },
  purple: { 50: '#faf5ff', 500: '#a855f7', 900: '#581c87' },
} as const;
ts
// tokens/primitives/spacing.ts
export const primitiveSpacing = {
  0: '0px',
  1: '4px',
  2: '8px',
  3: '12px',
  4: '16px',
  5: '20px',
  6: '24px',
  8: '32px',
  10: '40px',
  12: '48px',
  16: '64px',
  20: '80px',
  24: '96px',
  32: '128px',
  40: '160px',
  48: '192px',
  56: '224px',
  64: '256px',
} as const;
ts
// tokens/primitives/typography.ts
export const primitiveTypography = {
  fontFamily: {
    sans: ['Space Grotesk', 'system-ui', 'sans-serif'],
    serif: ['Georgia', 'Cambria', 'serif'],
    mono: ['Menlo', 'Monaco', 'monospace'],
  },
  fontSize: {
    xs: '0.75rem', // 12px
    sm: '0.875rem', // 14px
    base: '1rem', // 16px
    lg: '1.125rem', // 18px
    xl: '1.25rem', // 20px
    '2xl': '1.5rem', // 24px
    '3xl': '1.875rem', // 30px
    '4xl': '2.25rem', // 36px
    '5xl': '3rem', // 48px
    '6xl': '3.75rem', // 60px
    '7xl': '4.5rem', // 72px
  },
  fontWeight: {
    thin: '100',
    light: '300',
    normal: '400',
    medium: '500',
    semibold: '600',
    bold: '700',
    extrabold: '800',
    black: '900',
  },
  lineHeight: {
    none: '1',
    tight: '1.25',
    snug: '1.375',
    normal: '1.5',
    relaxed: '1.625',
    loose: '2',
  },
  letterSpacing: {
    tighter: '-0.05em',
    tight: '-0.025em',
    normal: '0',
    wide: '0.025em',
    wider: '0.05em',
    widest: '0.1em',
  },
} as const;

Layer 2: Semantic tokens

Meaningful tokens that reference primitives:

ts
// tokens/semantic/colors.ts
import { tokens } from 'typestyles';
import { primitiveColors } from '../primitives/colors';

export const color = tokens.create('color', {
  // Brand
  brand: primitiveColors.brand[500],
  brandHover: primitiveColors.brand[600],
  brandActive: primitiveColors.brand[700],
  brandSubtle: primitiveColors.brand[100],
  brandMuted: primitiveColors.brand[200],

  // Text
  text: primitiveColors.gray[900],
  textMuted: primitiveColors.gray[500],
  textSubtle: primitiveColors.gray[400],
  textInverse: '#ffffff',
  textBrand: primitiveColors.brand[600],

  // Backgrounds
  background: primitiveColors.gray[50],
  surface: '#ffffff',
  surfaceRaised: primitiveColors.gray[100],
  surfaceSunken: primitiveColors.gray[50],
  surfaceOverlay: 'rgba(0, 0, 0, 0.5)',

  // Borders
  border: primitiveColors.gray[200],
  borderHover: primitiveColors.gray[300],
  borderActive: primitiveColors.brand[500],

  // Status
  success: primitiveColors.green[500],
  successSubtle: primitiveColors.green[50],
  warning: primitiveColors.orange[500],
  warningSubtle: primitiveColors.orange[50],
  danger: primitiveColors.red[500],
  dangerSubtle: primitiveColors.red[50],
  info: primitiveColors.blue[500],
  infoSubtle: primitiveColors.blue[50],
});
ts
// tokens/semantic/spacing.ts
import { tokens } from 'typestyles';
import { primitiveSpacing } from '../primitives/spacing';

export const space = tokens.create('space', {
  // Base scale
  ...primitiveSpacing,

  // Semantic aliases
  gap: primitiveSpacing[4],
  gapSm: primitiveSpacing[2],
  gapLg: primitiveSpacing[6],

  section: primitiveSpacing[16],
  sectionSm: primitiveSpacing[12],
  sectionLg: primitiveSpacing[24],

  component: primitiveSpacing[4],
  componentSm: primitiveSpacing[3],
  componentLg: primitiveSpacing[6],
});

Split typography into separate token namespaces so scale names like normal and tight do not collide across weight, line height, and tracking (a single tokens.create('font', …) object cannot contain duplicate keys).

ts
// tokens/semantic/typography.ts
import { tokens } from 'typestyles';
import { primitiveTypography } from '../primitives/typography';

export const font = tokens.create('font', {
  sans: primitiveTypography.fontFamily.sans.join(', '),
  serif: primitiveTypography.fontFamily.serif.join(', '),
  mono: primitiveTypography.fontFamily.mono.join(', '),

  xs: primitiveTypography.fontSize.xs,
  sm: primitiveTypography.fontSize.sm,
  base: primitiveTypography.fontSize.base,
  lg: primitiveTypography.fontSize.lg,
  xl: primitiveTypography.fontSize.xl,
  '2xl': primitiveTypography.fontSize['2xl'],
  '3xl': primitiveTypography.fontSize['3xl'],
  '4xl': primitiveTypography.fontSize['4xl'],

  body: primitiveTypography.fontSize.base,
  bodySm: primitiveTypography.fontSize.sm,
  bodyLg: primitiveTypography.fontSize.lg,
  headingSm: primitiveTypography.fontSize.xl,
  heading: primitiveTypography.fontSize['2xl'],
  headingLg: primitiveTypography.fontSize['3xl'],
  headingXl: primitiveTypography.fontSize['4xl'],
  display: primitiveTypography.fontSize['5xl'],
  displayLg: primitiveTypography.fontSize['6xl'],
});

export const fontWeight = tokens.create('font-weight', {
  normal: primitiveTypography.fontWeight.normal,
  medium: primitiveTypography.fontWeight.medium,
  semibold: primitiveTypography.fontWeight.semibold,
  bold: primitiveTypography.fontWeight.bold,
});

export const lineHeight = tokens.create('line-height', {
  none: primitiveTypography.lineHeight.none,
  tight: primitiveTypography.lineHeight.tight,
  snug: primitiveTypography.lineHeight.snug,
  normal: primitiveTypography.lineHeight.normal,
  relaxed: primitiveTypography.lineHeight.relaxed,
  loose: primitiveTypography.lineHeight.loose,
});

export const letterSpacing = tokens.create('letter-spacing', {
  tighter: primitiveTypography.letterSpacing.tighter,
  tight: primitiveTypography.letterSpacing.tight,
  normal: primitiveTypography.letterSpacing.normal,
  wide: primitiveTypography.letterSpacing.wide,
  wider: primitiveTypography.letterSpacing.wider,
  widest: primitiveTypography.letterSpacing.widest,
});

Layer 3: Component tokens

Component-specific tokens:

ts
// tokens/components/button.ts
import { tokens } from 'typestyles';
import { color, space, font, fontWeight } from '../semantic';

export const button = tokens.create('button', {
  // Size
  heightSm: '32px',
  heightMd: '40px',
  heightLg: '48px',

  // Padding
  paddingHorizontalSm: space[3],
  paddingHorizontalMd: space[4],
  paddingHorizontalLg: space[6],
  paddingVerticalSm: space[2],
  paddingVerticalMd: space[2],
  paddingVerticalLg: space[3],

  // Border radius
  borderRadius: '6px',
  borderRadiusSm: '4px',
  borderRadiusLg: '8px',

  // Font
  fontSizeSm: font.sm,
  fontSizeMd: font.base,
  fontSizeLg: font.lg,
  fontWeight: fontWeight.medium,

  // Primary variant
  primaryBackground: color.brand,
  primaryBackgroundHover: color.brandHover,
  primaryBackgroundActive: color.brandActive,
  primaryText: color.textInverse,

  // Secondary variant
  secondaryBackground: color.surface,
  secondaryBackgroundHover: color.surfaceRaised,
  secondaryBorder: color.border,
  secondaryText: color.text,

  // Focus ring
  focusRingColor: color.brand,
  focusRingWidth: '2px',
  focusRingOffset: '2px',
});
ts
// tokens/components/card.ts
import { tokens } from 'typestyles';
import { color, space } from '../semantic';

export const card = tokens.create('card', {
  // Spacing
  padding: space[6],
  paddingSm: space[4],
  paddingLg: space[8],

  // Visual
  borderRadius: '8px',
  borderWidth: '1px',
  borderColor: color.border,
  background: color.surface,

  // Shadow
  shadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
  shadowHover: '0 4px 6px rgba(0, 0, 0, 0.1)',

  // Header
  headerPadding: space[4],
  headerBorderColor: color.border,

  // Body
  bodyPadding: space[6],

  // Footer
  footerPadding: space[4],
  footerBackground: color.surfaceRaised,
  footerBorderColor: color.border,
});

Token documentation

Token reference table

Create documentation for your tokens:

md
# Color Tokens

## Brand

| Token               | Value   | Usage                  |
| ------------------- | ------- | ---------------------- |
| `color.brand`       | #3b82f6 | Primary buttons, links |
| `color.brandHover`  | #2563eb | Hover states           |
| `color.brandSubtle` | #dbeafe | Light backgrounds      |

## Text

| Token               | Value   | Usage                    |
| ------------------- | ------- | ------------------------ |
| `color.text`        | #111827 | Primary text             |
| `color.textMuted`   | #6b7280 | Secondary text           |
| `color.textInverse` | #ffffff | Text on dark backgrounds |

## Status

| Token           | Value   | Usage          |
| --------------- | ------- | -------------- |
| `color.success` | #22c55e | Success states |
| `color.danger`  | #ef4444 | Error states   |

Token usage examples

ts
// Always use tokens, never hardcode values

// ❌ Bad
const button = styles.component('button', {
  base: {
    padding: '8px 16px', // Hardcoded
    backgroundColor: '#3b82f6', // Hardcoded
  },
});

// ✅ Good
const button = styles.component('button', {
  base: {
    padding: `${buttonToken.paddingHorizontalMd} ${buttonToken.paddingVerticalMd}`,
    backgroundColor: buttonToken.primaryBackground,
  },
});

Theme variations

Dark theme

ts
// tokens/themes/dark.ts
import { tokens } from 'typestyles';
import { primitiveColors } from '../primitives/colors';

export const darkTheme = tokens.createTheme('dark', {
  base: {
    color: {
      brand: primitiveColors.brand[400],
      brandHover: primitiveColors.brand[300],
      brandActive: primitiveColors.brand[500],

      text: primitiveColors.gray[100],
      textMuted: primitiveColors.gray[400],
      textSubtle: primitiveColors.gray[500],
      textInverse: primitiveColors.gray[900],

      background: primitiveColors.gray[900],
      surface: primitiveColors.gray[800],
      surfaceRaised: primitiveColors.gray[700],
      surfaceSunken: primitiveColors.gray[950],

      border: primitiveColors.gray[700],
      borderHover: primitiveColors.gray[600],
    },
  },
});

High contrast theme

ts
// tokens/themes/high-contrast.ts
import { tokens } from 'typestyles';

export const highContrastTheme = tokens.createTheme('high-contrast', {
  base: {
    color: {
      text: '#000000',
      background: '#ffffff',
      border: '#000000',
      brand: '#0000ff',

      success: '#006600',
      warning: '#cc6600',
      danger: '#cc0000',
    },
  },
});

Token naming conventions

Consistent naming

Category + Property + Variant + State

Examples:
- color.text (category.property)
- color.textMuted (category.property.variant)
- color.textMutedHover (category.property.variant.state)
- button.primaryBackground (category.property)
- button.primaryBackgroundHover (category.property.state)

Property modifiers

Base: color.brand
Subtle: color.brandSubtle
Muted: color.brandMuted
Hover: color.brandHover
Active: color.brandActive
Disabled: color.brandDisabled
Inverse: color.brandInverse

Token validation

Type checking

ts
// Ensure all required tokens exist
interface RequiredTokens {
  brand: string;
  text: string;
  background: string;
  // ... etc
}

// TypeScript will catch missing tokens
const requiredTokens: RequiredTokens = {
  brand: color.brand,
  text: color.text,
  background: color.background,
};

Contrast checking

ts
// WCAG contrast ratio helper
function getContrastRatio(color1: string, color2: string): number {
  // Implementation
}

// Ensure accessible contrast
const contrastRatio = getContrastRatio(color.text, color.background);
if (contrastRatio < 4.5) {
  console.warn('Text contrast does not meet WCAG AA standards');
}

Token distribution

NPM package structure

@myorg/design-tokens/
├── package.json
├── tokens/
│   ├── primitives/
│   ├── semantic/
│   ├── components/
│   └── themes/
└── dist/
    ├── tokens.css      # CSS custom properties
    ├── tokens.json     # JSON format
    └── tokens.scss     # SCSS variables

Multiple formats

TypeStyles does not ship a separate “token compiler” — your tokens.create calls emit CSS custom properties when the module runs (or when an extraction build evaluates your entry). For additional artifacts (JSON for Figma pipelines, SCSS for legacy apps), treat primitives as the source of truth in TypeScript and add your own small exporters.

CSS (what TypeStyles already produces)

  • Diagnostics / testsgetRegisteredCss() from typestyles after importing your token modules.
  • SSRcollectStyles() from typestyles/server around renderToString, then inject the returned CSS string.
  • Production bundle@typestyles/vite, @typestyles/rollup, or @typestyles/next with an extraction entry so a static .css file includes :root and theme rules.

JSON / SCSS

  • Export plain objects from tokens/primitives/*.ts and JSON.stringify them, or generate SCSS variables from those objects in a script. Keep TypeStyles as the runtime source of truth for var(--…) values that components actually use.

Token governance

Change process

  1. Request: Designer/developer requests a new token
  2. Review: Design system team reviews for necessity
  3. Naming: Follow naming conventions
  4. Documentation: Update token reference
  5. Communication: Announce breaking changes
  6. Deprecation: Mark old tokens as deprecated before removal

Versioning

json
{
  "name": "@myorg/design-tokens",
  "version": "2.1.0",
  "description": "Design tokens for the Acme design system"
}

Breaking changes (new major version):

  • Removing tokens
  • Renaming tokens
  • Changing token values significantly

Non-breaking changes (new minor/patch):

  • Adding new tokens
  • Small value adjustments
  • Adding new themes

Best practices summary

  1. Never hardcode values - Always use tokens
  2. Use semantic names - color.text not color.gray900
  3. Layer your tokens - Primitives → Semantics → Components
  4. Document everything - Maintain token reference
  5. Test contrast - Ensure accessibility compliance
  6. Version carefully - Follow semantic versioning
  7. Support multiple themes - Light, dark, high-contrast
  8. Generate multiple formats - CSS, SCSS, JSON for different consumers
  9. Govern changes - Have a process for token modifications
  10. Provide examples - Show developers how to use tokens