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 accesstokens.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.