Skip to main content

DESIGN TOKENS

design-tokens.ts

Semantic color/type tokens with one indirection layer, so a theme pivot is a token change, not a component-wide find-replace.

Stark avatarStark

WHAT THIS PATTERN TEACHES

How to scope all color and type to two layers: PRIMITIVES (the only place raw values live) and SEMANTIC roles that point at primitives. Components reference semantic tokens via CSS custom properties only — never hex or pixel literals — so a rebrand or dark theme becomes a single token edit.

WHEN TO USE THIS

Any design system or app that will rebrand, add a dark theme, or pivot its palette/type scale. Pairs with component.tsx (components consume tokens) and combobox.tsx (a11y-critical surfaces). Add a lint guardrail forbidding raw hex/px in component source.

AT A GLANCE

const tokenColor = (role) => `var(--vf-color-${role.replace(/[.\s]+/g, '-')})`
// component references SEMANTIC tokens only — never a hex, never a pixel literal
style = { background: tokenColor('accent'), color: tokenColor('text.on-accent') }

FRAMEWORK IMPLEMENTATIONS

TypeScript
// Layer 1: Primitives — the ONLY place raw values live. Named by what they
// ARE (a swatch index, a scale step), never by what they're FOR.
export const primitives = {
  color: {
    white: '#ffffff', black: '#0a0a0a',
    gray50: '#f9fafb', gray200: '#e5e7eb', gray500: '#6b7280',
    gray700: '#374151', gray900: '#111827',
    indigo500: '#6366f1', indigo600: '#4f46e5', indigo700: '#4338ca',
    red500: '#ef4444', red600: '#dc2626', green500: '#22c55e', amber500: '#f59e0b',
  },
  fontSize: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.5rem' },
} as const;

type PrimitiveColor = keyof typeof primitives.color;

// Layer 2: Semantic tokens — a NAME FOR A ROLE that points at a primitive.
// The shape is the contract: a theme override must provide EVERY role, so the
// type system guarantees no role is left un-themed.
export type SemanticColors = {
  'bg.canvas': PrimitiveColor;
← All Patterns