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-gapPrimitive 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.colorActionworks because of DeepSignal proxy, but TypeScript can't verify thatcolorActionexists on the token object at compile time. Use theinterfacepattern 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
colorTexthas sufficient contrast againstcolorBg. Use external tools.No token documentation generator — you'll need to build a Storybook-like catalog yourself or document tokens in markdown.