Design Tokens

Design tokens are the atomic values of your design system — colors, spacing, typography, radii. They ensure consistency across a large team and enable multi-brand theming.

CellUI doesn't ship a token system. You build one with ambient() and plain objects. This guide shows the architecture.

Token Layers

A production token system has three layers:

Primitive   → Semantic      → Component
teal-500    → color-action  → button-bg-primary
gray-900    → color-surface → card-bg
16px        → spacing-md    → form-field-gap

Primitive tokens are raw values: colors, sizes, fonts. Brand-agnostic.

Semantic tokens map primitives to intent: "this color means action," "this spacing is for component internals." Brand-aware.

Component tokens apply semantic tokens to specific components. Optional — many teams skip this layer and use semantic tokens directly.

Implementation

typescriptimport { ambient } from '@cmj/cellui';

// ─── Layer 1: Primitives ───────────────────────
// These never change per brand. They're your palette.

const primitives = {
  teal50: '#f0fdfa', teal100: '#ccfbf1', teal200: '#99f6e4',
  teal300: '#5eead4', teal400: '#2dd4bf', teal500: '#14b8a6',
  teal600: '#0d9488', teal700: '#0f766e', teal800: '#115e59',

  gray50: '#fafafa', gray100: '#f4f4f5', gray200: '#e4e4e7',
  gray300: '#d4d4d8', gray400: '#a1a1aa', gray500: '#71717a',
  gray600: '#52525b', gray700: '#3f3f46', gray800: '#27272a',
  gray900: '#18181b', gray950: '#09090b',

  red500: '#ef4444', green500: '#22c55e', yellow500: '#eab308',

  space0: '0px', space1: '4px', space2: '8px', space3: '12px',
  space4: '16px', space5: '20px', space6: '24px', space8: '32px',
  space10: '40px', space12: '48px', space16: '64px',

  radiusSm: '4px', radiusMd: '8px', radiusLg: '12px', radiusFull: '9999px',

  fontSans: "'Inter', system-ui, sans-serif",
  fontMono: "'JetBrains Mono', monospace",

  textXs: '12px', textSm: '13px', textBase: '15px',
  textLg: '18px', textXl: '22px', text2xl: '28px', text3xl: '36px',
};

// ─── Layer 2: Semantic (dark theme) ────────────
// These change per theme. Map intent to primitives.

const darkSemantic = {
  colorBg: primitives.gray950,
  colorSurface: primitives.gray900,
  colorSurfaceRaised: primitives.gray800,
  colorBorder: primitives.gray800,
  colorBorderSubtle: primitives.gray700,

  colorText: primitives.gray50,
  colorTextSecondary: primitives.gray400,
  colorTextMuted: primitives.gray500,

  colorAction: primitives.teal500,
  colorActionHover: primitives.teal600,
  colorActionText: '#ffffff',

  colorDanger: primitives.red500,
  colorSuccess: primitives.green500,
  colorWarning: primitives.yellow500,

  spacingXs: primitives.space1,
  spacingSm: primitives.space2,
  spacingMd: primitives.space4,
  spacingLg: primitives.space6,
  spacingXl: primitives.space10,

  radius: primitives.radiusMd,
  radiusLg: primitives.radiusLg,

  fontBody: primitives.fontSans,
  fontCode: primitives.fontMono,
  textBody: primitives.textBase,
  textHeading: primitives.text2xl,
};

// ─── Layer 2: Semantic (light theme) ───────────

const lightSemantic = {
  ...darkSemantic,
  colorBg: '#ffffff',
  colorSurface: primitives.gray50,
  colorSurfaceRaised: primitives.gray100,
  colorBorder: primitives.gray200,
  colorBorderSubtle: primitives.gray100,
  colorText: primitives.gray900,
  colorTextSecondary: primitives.gray600,
  colorTextMuted: primitives.gray400,
};

// ─── Register as ambient ───────────────────────

const tokens = ambient('design-tokens', darkSemantic);

Using Tokens in Components

typescriptfunction Card(props: { title: string; children: () => Node }) {
  return view`
    <div style="
      background: ${tokens.colorSurface};
      border: 1px solid ${tokens.colorBorder};
      border-radius: ${tokens.radius};
      padding: ${tokens.spacingLg};
    ">
      <h3 style="color: ${tokens.colorText}; font-size: ${tokens.textHeading}; margin-bottom: ${tokens.spacingMd};">
        ${props.title}
      </h3>
      <div style="color: ${tokens.colorTextSecondary};">
        ${props.children()}
      </div>
    </div>
  `;
}

Every value comes from the token system. When tokens.value = lightSemantic, every component updates.

Syncing Tokens to CSS Custom Properties

For components that use CSS classes instead of inline styles:

typescriptimport { effect } from '@cmj/cellui';

effect(() => {
  const t = tokens.value;
  const s = document.documentElement.style;
  s.setProperty('--color-bg', t.colorBg);
  s.setProperty('--color-surface', t.colorSurface);
  s.setProperty('--color-border', t.colorBorder);
  s.setProperty('--color-text', t.colorText);
  s.setProperty('--color-text-secondary', t.colorTextSecondary);
  s.setProperty('--color-action', t.colorAction);
  s.setProperty('--color-danger', t.colorDanger);
  s.setProperty('--spacing-md', t.spacingMd);
  s.setProperty('--spacing-lg', t.spacingLg);
  s.setProperty('--radius', t.radius);
});

Now your CSS can use var(--color-action) and it updates when the theme switches.

Multi-Brand Theming

For white-label products where each customer has different colors:

typescriptinterface BrandConfig {
  name: string;
  primary: string;
  primaryHover: string;
  logo: string;
}

function applyBrand(brand: BrandConfig) {
  const current = tokens.value;
  tokens.value = {
    ...current,
    colorAction: brand.primary,
    colorActionHover: brand.primaryHover,
  };
}

// Customer A: blue brand
applyBrand({ name: 'Acme', primary: '#2563eb', primaryHover: '#1d4ed8', logo: '/acme.svg' });

// Customer B: orange brand
applyBrand({ name: 'Globex', primary: '#ea580c', primaryHover: '#c2410c', logo: '/globex.svg' });

All components using tokens.colorAction update instantly. No rebuild, no CSS regeneration.

Scoped Overrides with Substrates

For sections of your app that need different tokens (e.g., a dark sidebar in a light app):

typescriptimport { createSubstrate, signal } from '@cmj/cellui';

function DarkSection(content: () => Node) {
  const scope = createSubstrate('dark-section');
  const bg = scope.connect('bg', signal(primitives.gray950));
  const text = scope.connect('text', signal(primitives.gray50));

  const container = document.createElement('div');
  container.style.background = bg.value;
  container.style.color = text.value;
  container.appendChild(content());

  // Reactive: if scope values change, update inline styles
  bg.subscribe(v => container.style.background = v);
  text.subscribe(v => container.style.color = v);

  return container;
}

What This Doesn't Do

  • No type-safe token access — tokens.colorAction works because of DeepSignal proxy, but TypeScript can't verify that colorAction exists on the token object at compile time. Use the interface pattern shown above to get autocomplete.

  • No design tool sync — there's no Figma plugin that generates CellUI tokens. Export from Figma as JSON, map to the primitive layer manually.

  • No automatic contrast checking — the token system doesn't validate that colorText has sufficient contrast against colorBg. Use external tools.

  • No token documentation generator — you'll need to build a Storybook-like catalog yourself or document tokens in markdown.