Skip to content
HELiX

Design Token Architecture

apps/docs/src/content/docs/components/styling/tokens Click to copy
Copied! apps/docs/src/content/docs/components/styling/tokens

Design tokens are the single source of truth for all visual design decisions in HELiX. They form a cascading three-tier system that enables global theming, component-level customization, and multi-brand deployments without modifying component internals.

This guide covers the complete token architecture: primitive tokens, semantic tokens, component tokens, naming conventions, fallback chains, token categories (color, spacing, typography, timing), real-world examples from HELiX, and theming strategies for consumers.


Before diving into token architecture, ensure you understand:

  • Component Styling Fundamentals — Shadow DOM styling, :host selectors, CSS custom properties
  • Basic CSS custom properties (--property-name syntax)
  • CSS cascade and inheritance rules

Design tokens are named entities that store visual design decisions. Instead of hardcoding values like #2563eb or 0.5rem directly in component styles, you reference tokens like --hx-color-primary-500 or --hx-space-2.

Without tokens:

/* Component styles — hardcoded values */
.button {
background: #2563eb;
padding: 0.5rem 1rem;
font-weight: 600;
border-radius: 0.375rem;
}

Problems:

  • Changing the brand color requires editing every component
  • No way to support dark mode without rewriting styles
  • Impossible to create white-label themes
  • Design decisions buried in implementation details

With tokens:

/* Component styles — token-based */
.button {
background: var(--hx-button-bg, var(--hx-color-primary-500, #2563eb));
padding: var(--hx-space-2, 0.5rem) var(--hx-space-4, 1rem);
font-weight: var(--hx-button-font-weight, var(--hx-font-weight-semibold, 600));
border-radius: var(--hx-button-border-radius, var(--hx-border-radius-md, 0.375rem));
}

Benefits:

  • Global theme change: override --hx-color-primary-500 once
  • Dark mode: swap token values, components adapt automatically
  • White-label: each brand defines their own token values
  • Design decisions are explicit and documented

CSS custom properties (the mechanism behind tokens) are unique among CSS properties: they inherit across shadow boundaries. This is not a bug — it is the design.

<style>
:root {
--hx-color-primary-500: #007878; /* Teal brand color */
}
</style>
<hx-button>
#shadow-root
<style>
.button {
background: var(--hx-color-primary-500, #2563eb);
/* Resolves to #007878 (inherited from :root) */
}
</style>
<button class="button">Click Me</button>
</hx-button>

Even though the shadow boundary blocks external selectors from reaching into the component, CSS custom properties inherit naturally from parent to child, crossing shadow boundaries. This makes tokens the perfect theming mechanism for Web Components.


HELiX uses a three-tier cascade that separates raw values (primitives), purpose-based values (semantic), and component-specific overrides (component tokens).

┌──────────────────────────────────────────────────────────────┐
│ TIER 1: PRIMITIVE TOKENS │
│ Raw values — #2563eb, 1rem, 600 │
│ No semantic meaning — "blue-500" not "primary" │
│ PRIVATE — never exposed to consumers │
└──────────────────────────────────────────────────────────────┘
│ (aliased by)
┌──────────────────────────────────────────────────────────────┐
│ TIER 2: SEMANTIC TOKENS │
│ Purpose-based — --hx-color-primary-500, --hx-space-4 │
│ PUBLIC API — the primary theming interface │
│ Mode-aware — light/dark/high-contrast variants │
└──────────────────────────────────────────────────────────────┘
│ (aliased by)
┌──────────────────────────────────────────────────────────────┐
│ TIER 3: COMPONENT TOKENS │
│ Component-specific — --hx-button-bg, --hx-card-padding │
│ OPTIONAL — defaults to semantic tokens if unset │
│ Override points — surgical component customization │
└──────────────────────────────────────────────────────────────┘

One tier (only primitives):

  • Consumers must know which blue shade to use for every context
  • No consistency: button might use blue-500, card uses blue-600
  • Theming requires changing 50+ individual values

Two tiers (primitives + component):

  • No global theming layer
  • To change the primary color, you must override every component token individually
  • Duplicated work: --hx-button-bg, --hx-link-color, --hx-badge-bg all set to the same value

Three tiers (primitives + semantic + component):

  • Global theming: Override --hx-color-primary-500 once, all components adapt
  • Component precision: Override --hx-button-bg for one specific component
  • Consistency: All “primary” uses reference the same semantic token by default
  • Flexibility: Choose the right level of granularity for each use case

Primitive tokens define what styles are available. They are raw, context-free values that form the palette and scales of the design system.

In @helixui/tokens, primitives live as TypeScript constants in packages/hx-tokens/src/ — not as CSS custom properties. The build step inlines them into the semantic CSS (dist/tokens.css) at publish time, so the shipped surface is the semantic layer (Tier 2). Consumers never reference primitives directly because they aren’t exposed as CSS variables.

  • Build-time only — TypeScript constants, not runtime CSS custom properties
  • Context-free — No semantic meaning (e.g., “blue-500” not “primary”)
  • Comprehensive — Full palette/scale, not just what’s currently used
  • Hardcoded fallbacks — Inlined as the last resort in semantic-tier var() chains
// packages/hx-tokens/src/colors/* (conceptual shape — names baked into Tier 2)
const blue = {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6', // Base blue
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
};
const neutral = {
0: '#ffffff',
50: '#f8f9fa',
100: '#f1f5f9',
200: '#e9ecef',
300: '#dee2e6',
400: '#ced4da',
500: '#adb5bd',
600: '#6c757d',
700: '#495057',
800: '#343a40',
900: '#212529',
950: '#1a1a1a',
};
/* 4px base unit scale */
--hx-spacing-0: 0;
--hx-spacing-px: 1px;
--hx-spacing-0-5: 0.125rem; /* 2px */
--hx-spacing-1: 0.25rem; /* 4px */
--hx-spacing-1-5: 0.375rem; /* 6px */
--hx-spacing-2: 0.5rem; /* 8px */
--hx-spacing-3: 0.75rem; /* 12px */
--hx-spacing-4: 1rem; /* 16px */
--hx-spacing-5: 1.25rem; /* 20px */
--hx-spacing-6: 1.5rem; /* 24px */
--hx-spacing-8: 2rem; /* 32px */
--hx-spacing-10: 2.5rem; /* 40px */
--hx-spacing-12: 3rem; /* 48px */
--hx-spacing-16: 4rem; /* 64px */
--hx-spacing-20: 5rem; /* 80px */
--hx-spacing-24: 6rem; /* 96px */
/* Font families */
--hx-font-family-sans-primitive:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--hx-font-family-serif-primitive: Georgia, 'Times New Roman', Times, serif;
--hx-font-family-mono-primitive: 'Courier New', Courier, monospace;
/* Font weights */
--hx-font-weight-normal-primitive: 400;
--hx-font-weight-medium-primitive: 500;
--hx-font-weight-semibold-primitive: 600;
--hx-font-weight-bold-primitive: 700;
/* Font sizes (Major Third scale, ratio 1.250) */
--hx-font-size-2xs-primitive: 0.625rem; /* 10px */
--hx-font-size-xs-primitive: 0.75rem; /* 12px */
--hx-font-size-sm-primitive: 0.875rem; /* 14px */
--hx-font-size-md-primitive: 1rem; /* 16px */
--hx-font-size-lg-primitive: 1.125rem; /* 18px */
--hx-font-size-xl-primitive: 1.25rem; /* 20px */
--hx-font-size-2xl-primitive: 1.5rem; /* 24px */
--hx-font-size-3xl-primitive: 1.875rem; /* 30px */
--hx-font-size-4xl-primitive: 2.25rem; /* 36px */
--hx-font-size-5xl-primitive: 3rem; /* 48px */
/* Line heights */
--hx-line-height-tight-primitive: 1.25;
--hx-line-height-snug-primitive: 1.375;
--hx-line-height-normal-primitive: 1.5;
--hx-line-height-relaxed-primitive: 1.625;
--hx-line-height-loose-primitive: 2;
/* Border radii */
--hx-border-radius-none-primitive: 0;
--hx-border-radius-sm-primitive: 0.125rem; /* 2px */
--hx-border-radius-md-primitive: 0.25rem; /* 4px */
--hx-border-radius-lg-primitive: 0.375rem; /* 6px */
--hx-border-radius-xl-primitive: 0.5rem; /* 8px */
--hx-border-radius-2xl-primitive: 0.75rem; /* 12px */
--hx-border-radius-full-primitive: 9999px;
/* Border widths */
--hx-border-width-thin-primitive: 1px;
--hx-border-width-medium-primitive: 2px;
--hx-border-width-thick-primitive: 4px;
/* Shadows */
--hx-shadow-sm-primitive: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--hx-shadow-md-primitive: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--hx-shadow-lg-primitive: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--hx-shadow-xl-primitive: 0 20px 25px -5px rgb(0 0 0 / 0.1);
--hx-shadow-2xl-primitive: 0 25px 50px -12px rgb(0 0 0 / 0.25);
/* Transitions */
--hx-transition-fast-primitive: 150ms ease;
--hx-transition-normal-primitive: 250ms ease;
--hx-transition-slow-primitive: 350ms ease;
  1. Never expose primitives to consumers — They are internal implementation details
  2. Use descriptive scale namingblue-500 not primary-blue
  3. Provide comprehensive scales — Include shades you don’t currently use (future-proofing)
  4. No semantic meaningblue-600 does not imply “button background”

Semantic tokens define how primitive values are applied. They carry purpose and meaning. These are the public API of the design system — the tokens that consumers and theme authors interact with.

  • Public — Exposed to consumers for theming
  • Purpose-driven — Names describe intent (e.g., color-primary-500 not blue-600)
  • Mode-aware — Can have different values in light/dark/high-contrast modes
  • Consistency — Used consistently across all components
/* Primary color (brand identity) — primitive hexes inlined at build time. */
--hx-color-primary-50: #eff6ff;
--hx-color-primary-100: #dbeafe;
--hx-color-primary-200: #bfdbfe;
--hx-color-primary-300: #93c5fd;
--hx-color-primary-400: #60a5fa;
--hx-color-primary-500: #3b82f6;
--hx-color-primary-600: #2563eb;
--hx-color-primary-700: #1d4ed8;
--hx-color-primary-800: #1e40af;
--hx-color-primary-900: #1e3a8a;
--hx-color-primary-950: #172554;
/* Neutral colors (text, backgrounds, borders) — primitives inlined. */
--hx-color-neutral-0: #ffffff;
--hx-color-neutral-50: #f8f9fa;
--hx-color-neutral-100: #f1f5f9;
--hx-color-neutral-200: #e9ecef;
--hx-color-neutral-300: #dee2e6;
--hx-color-neutral-400: #ced4da;
--hx-color-neutral-500: #adb5bd;
--hx-color-neutral-600: #6c757d;
--hx-color-neutral-700: #495057;
--hx-color-neutral-800: #343a40;
--hx-color-neutral-900: #212529;
/* Success (green) */
--hx-color-success-50: #ecfdf5;
--hx-color-success-100: #d1fae5;
--hx-color-success-500: #10b981;
--hx-color-success-700: #15803d;
--hx-color-success-800: #166534;
/* Warning (yellow/amber) */
--hx-color-warning-50: #fffbeb;
--hx-color-warning-100: #fef3c7;
--hx-color-warning-500: #f59e0b;
--hx-color-warning-700: #b45309;
--hx-color-warning-800: #92400e;
/* Error (red) */
--hx-color-error-50: #fef2f2;
--hx-color-error-100: #fee2e2;
--hx-color-error-500: #ef4444;
--hx-color-error-700: #b91c1c;
--hx-color-error-800: #991b1b;
/* Info (blue) */
--hx-color-info-50: #e8f4fd;
--hx-color-info-100: #d1e9fb;
--hx-color-info-500: #3b82f6;
--hx-color-info-700: #1d4ed8;
--hx-color-info-800: #1e40af;
/* Space scale (general-purpose spacing) */
--hx-space-0: 0;
--hx-space-px: 1px;
--hx-space-0-5: 0.125rem;
--hx-space-1: 0.25rem;
--hx-space-1-5: 0.375rem;
--hx-space-2: 0.5rem;
--hx-space-3: 0.75rem;
--hx-space-4: 1rem;
--hx-space-5: 1.25rem;
--hx-space-6: 1.5rem;
--hx-space-8: 2rem;
--hx-space-10: 2.5rem;
--hx-space-12: 3rem;
--hx-space-16: 4rem;
--hx-space-20: 5rem;
--hx-space-24: 6rem;
/* Size scale (for fixed dimensions like min-height) */
--hx-size-4: 1rem; /* 16px */
--hx-size-5: 1.25rem; /* 20px */
--hx-size-8: 2rem; /* 32px */
--hx-size-10: 2.5rem; /* 40px */
--hx-size-12: 3rem; /* 48px */
--hx-size-16: 4rem; /* 64px */
--hx-size-20: 5rem; /* 80px */
/* Font families */
--hx-font-family-sans: var(--hx-font-family-sans-primitive, system-ui, sans-serif);
--hx-font-family-serif: var(--hx-font-family-serif-primitive, Georgia, serif);
--hx-font-family-mono: var(--hx-font-family-mono-primitive, 'Courier New', monospace);
/* Font weights */
--hx-font-weight-normal: var(--hx-font-weight-normal-primitive, 400);
--hx-font-weight-medium: var(--hx-font-weight-medium-primitive, 500);
--hx-font-weight-semibold: var(--hx-font-weight-semibold-primitive, 600);
--hx-font-weight-bold: var(--hx-font-weight-bold-primitive, 700);
/* Font sizes */
--hx-font-size-2xs: var(--hx-font-size-2xs-primitive, 0.625rem);
--hx-font-size-xs: var(--hx-font-size-xs-primitive, 0.75rem);
--hx-font-size-sm: var(--hx-font-size-sm-primitive, 0.875rem);
--hx-font-size-md: var(--hx-font-size-md-primitive, 1rem);
--hx-font-size-lg: var(--hx-font-size-lg-primitive, 1.125rem);
--hx-font-size-xl: var(--hx-font-size-xl-primitive, 1.25rem);
--hx-font-size-2xl: var(--hx-font-size-2xl-primitive, 1.5rem);
--hx-font-size-3xl: var(--hx-font-size-3xl-primitive, 1.875rem);
--hx-font-size-4xl: var(--hx-font-size-4xl-primitive, 2.25rem);
--hx-font-size-5xl: var(--hx-font-size-5xl-primitive, 3rem);
/* Line heights */
--hx-line-height-tight: var(--hx-line-height-tight-primitive, 1.25);
--hx-line-height-snug: var(--hx-line-height-snug-primitive, 1.375);
--hx-line-height-normal: var(--hx-line-height-normal-primitive, 1.5);
--hx-line-height-relaxed: var(--hx-line-height-relaxed-primitive, 1.625);
--hx-line-height-loose: var(--hx-line-height-loose-primitive, 2);
/* Border radii */
--hx-border-radius-none: 0;
--hx-border-radius-sm: 0.125rem;
--hx-border-radius-md: 0.25rem;
--hx-border-radius-lg: 0.375rem;
--hx-border-radius-xl: 0.5rem;
--hx-border-radius-2xl: 0.75rem;
--hx-border-radius-full: 9999px;
/* Border widths */
--hx-border-width-thin: 1px;
--hx-border-width-medium: 2px;
--hx-border-width-thick: 4px;
/* Shadows */
--hx-shadow-none: none;
--hx-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--hx-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--hx-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--hx-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
--hx-shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
/* Transitions */
--hx-transition-fast: 150ms ease;
--hx-transition-normal: 250ms ease;
--hx-transition-slow: 350ms ease;
/* Focus ring */
--hx-focus-ring-width: 2px;
--hx-focus-ring-offset: 2px;
--hx-focus-ring-color: var(--hx-color-primary-500, #2563eb);
--hx-focus-ring-opacity: 0.25;
/* Opacity */
--hx-opacity-disabled: 0.5;
--hx-opacity-hover: 0.9;
--hx-opacity-active: 0.8;
/* Transforms */
--hx-transform-lift-sm: -1px;
--hx-transform-lift-md: -2px;
--hx-transform-lift-lg: -4px;
  1. Name by purpose, not appearance--hx-color-primary-500 not --hx-color-blue
  2. Provide complete scales — Include all shades of each palette
  3. Mode-aware defaults — Semantic tokens can change in dark mode
  4. Document thoroughly — Every token has JSDoc describing usage

Component tokens define where semantic values are applied to specific component surfaces. They act as override points — if unset, they fall back to their semantic alias.

  • Optional — Components work fine with only semantic tokens
  • Surgical — Override one component without affecting others
  • Fallback — Always alias to semantic tokens by default
  • Documented — Listed in component JSDoc and CEM
/* hx-button tokens (all optional) */
--hx-button-bg: var(--hx-color-primary-500, #2563eb);
--hx-button-color: var(--hx-color-neutral-0, #ffffff);
--hx-button-border-color: transparent;
--hx-button-border-radius: var(--hx-border-radius-md, 0.375rem);
--hx-button-font-family: var(--hx-font-family-sans, sans-serif);
--hx-button-font-weight: var(--hx-font-weight-semibold, 600);
--hx-button-focus-ring-color: var(--hx-focus-ring-color, #2563eb);
--hx-button-padding-x: var(--hx-space-4, 1rem);
--hx-button-padding-y: var(--hx-space-2, 0.5rem);

Usage in hx-button component:

hx-button.styles.ts
export const wcButtonStyles = css`
:host {
display: inline-block;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--hx-space-2, 0.5rem);
padding: var(--hx-space-2, 0.5rem) var(--hx-space-4, 1rem);
/* Component token → Semantic token → Primitive fallback */
background-color: var(--hx-button-bg, var(--hx-color-primary-500, #2563eb));
color: var(--hx-button-color, var(--hx-color-neutral-0, #ffffff));
border: var(--hx-border-width-thin, 1px) solid var(--hx-button-border-color, transparent);
border-radius: var(--hx-button-border-radius, var(--hx-border-radius-md, 0.375rem));
font-family: var(--hx-button-font-family, var(--hx-font-family-sans, sans-serif));
font-weight: var(--hx-button-font-weight, var(--hx-font-weight-semibold, 600));
cursor: pointer;
transition:
background-color var(--hx-transition-fast, 150ms ease),
color var(--hx-transition-fast, 150ms ease),
border-color var(--hx-transition-fast, 150ms ease);
}
.button:focus-visible {
outline: var(--hx-focus-ring-width, 2px) solid
var(--hx-button-focus-ring-color, var(--hx-focus-ring-color, #2563eb));
outline-offset: var(--hx-focus-ring-offset, 2px);
}
.button:hover {
filter: brightness(var(--hx-filter-brightness-hover, 0.9));
}
`;
/* hx-card tokens (all optional) */
--hx-card-bg: var(--hx-color-neutral-0, #ffffff);
--hx-card-color: var(--hx-color-neutral-800, #212529);
--hx-card-border-color: var(--hx-color-neutral-200, #dee2e6);
--hx-card-border-radius: var(--hx-border-radius-lg, 0.5rem);
--hx-card-padding: var(--hx-space-6, 1.5rem);
--hx-card-shadow: var(--hx-shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));
/* hx-text-input tokens (all optional) */
--hx-input-bg: var(--hx-color-neutral-0, #ffffff);
--hx-input-color: var(--hx-color-neutral-800, #212529);
--hx-input-border-color: var(--hx-color-neutral-300, #ced4da);
--hx-input-border-radius: var(--hx-border-radius-md, 0.375rem);
--hx-input-font-family: var(--hx-font-family-sans, sans-serif);
--hx-input-label-color: var(--hx-color-neutral-700, #343a40);
--hx-input-focus-ring-color: var(--hx-focus-ring-color, #2563eb);
--hx-input-error-color: var(--hx-color-error-500, #dc3545);
/* hx-alert tokens (all optional) */
--hx-alert-gap: var(--hx-space-3, 0.75rem);
--hx-alert-padding: var(--hx-space-4, 1rem);
--hx-alert-border-width: var(--hx-border-width-thin, 1px);
--hx-alert-border-radius: var(--hx-border-radius-md, 0.375rem);
--hx-alert-font-family: var(--hx-font-family-sans, sans-serif);
--hx-alert-bg: var(--hx-color-info-50, #e8f4fd);
--hx-alert-border-color: var(--hx-color-info-200, #b3d9ef);
--hx-alert-color: var(--hx-color-info-800, #1a3a4a);
--hx-alert-icon-color: var(--hx-color-info-500, #3b82f6);
  1. Always fallback to semantic tokens — Never hardcode component token values
  2. Flat hierarchy--hx-button-bg not --hx-button-primary-bg (variants handled in component CSS)
  3. Consistent naming{component}-{property} pattern
  4. Optional adoption — Add component tokens only when consumers need them

All HELiX tokens follow a strict naming pattern for consistency and discoverability.

--{prefix}-{category}-{property}-{variant?}-{state?}
SegmentRequiredDescriptionExamples
prefixYesNamespace (hx)hx
categoryYesToken categorycolor, space, font
propertyYesSpecific propertyprimary, neutral, size
variantOptionalShade/scale position50, 100, 500, 900
stateOptionalInteractive statehover, active, focus

Pattern: --hx-color-{palette}-{shade}

--hx-color-primary-500 /* Primary brand color (base) */
--hx-color-primary-50 /* Very light primary */
--hx-color-primary-900 /* Very dark primary */
--hx-color-neutral-0 /* Pure white */
--hx-color-neutral-800 /* Near black */
--hx-color-success-500 /* Success green */
--hx-color-error-500 /* Error red */
--hx-color-warning-500 /* Warning yellow/amber */
--hx-color-info-500 /* Info blue */

Pattern: --hx-space-{scale} or --hx-size-{scale}

--hx-space-1 /* 0.25rem = 4px */
--hx-space-2 /* 0.5rem = 8px */
--hx-space-4 /* 1rem = 16px */
--hx-space-6 /* 1.5rem = 24px */
--hx-size-10 /* Fixed size 2.5rem = 40px (for min-height, etc.) */
--hx-size-12 /* Fixed size 3rem = 48px */

Pattern: --hx-font-{property}-{scale}

--hx-font-family-sans /* Sans-serif stack */
--hx-font-family-serif /* Serif stack */
--hx-font-family-mono /* Monospace stack */
--hx-font-weight-normal /* 400 */
--hx-font-weight-semibold /* 600 */
--hx-font-weight-bold /* 700 */
--hx-font-size-xs /* Extra small */
--hx-font-size-sm /* Small */
--hx-font-size-md /* Medium (base) */
--hx-font-size-lg /* Large */
--hx-font-size-xl /* Extra large */
--hx-line-height-tight /* 1.25 */
--hx-line-height-normal /* 1.5 */
--hx-line-height-loose /* 2 */

Pattern: --hx-{component}-{property}

--hx-button-bg /* Button background */
--hx-button-color /* Button text color */
--hx-button-border-radius /* Button corner radius */
--hx-card-bg /* Card background */
--hx-card-padding /* Card inner padding */
--hx-card-border-color /* Card border */
--hx-input-border-color /* Input border */
--hx-input-focus-ring-color /* Input focus ring */
--hx-input-error-color /* Input error state */
  1. All lowercase — No camelCase, PascalCase, or UPPER_CASE
  2. Hyphen-separated — Use - between segments
  3. Prefix always first--hx- comes before everything
  4. No abbreviations (except universal ones) — bg ✓, clr
  5. Scale as suffixprimary-500 not 500-primary
  6. State as final suffixprimary-hover not hover-primary

Every CSS custom property in HELiX uses a two-level fallback chain (or three-level for component tokens).

property: var(<semantic-token>, <primitive-value>);

Example:

.button {
background: var(--hx-color-primary-500, #2563eb);
/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Semantic token | Primitive fallback
*/
}

Resolution order:

  1. --hx-color-primary-500 (semantic token — can be overridden by consumer)
  2. #2563eb (primitive fallback — last resort if token unset)
property: var(<component-token>, var(<semantic-token>, <primitive-value>));

Example:

.button {
background: var(--hx-button-bg, var(--hx-color-primary-500, #2563eb));
/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Component token | Semantic token | Primitive fallback
*/
}

Resolution order:

  1. --hx-button-bg (component token — most specific, optional)
  2. --hx-color-primary-500 (semantic token — global theming)
  3. #2563eb (primitive fallback — last resort)

Problem: One level

/* One-level fallback */
.button {
background: var(--hx-button-bg, #2563eb);
}
  • Con: No global theming. To change the primary color, you must override every component token individually.
  • Con: Duplicates values across multiple component tokens.

Anti-pattern: skipping the semantic layer

/* Component → primitive hex directly — no semantic indirection. */
.button {
background: var(--hx-button-bg, #2563eb);
}
  • Con: No semantic layer means consumers who want a global theme retune have to override --hx-button-bg (and every sibling component token) directly. Add the semantic tier (--hx-color-primary-N) so one override retheme works across every component.

Solution: Two or three levels with semantic

/* Two levels (most common) */
.card {
background: var(--hx-color-neutral-0, #ffffff);
}
/* Three levels (for component customization) */
.button {
background: var(--hx-button-bg, var(--hx-color-primary-500, #2563eb));
}
  • Pro: Global theming works (override --hx-color-primary-500)
  • Pro: Component precision works (override --hx-button-bg)
  • Pro: Fallback works (primitive value if everything unset)

Spacing:

.card {
padding: var(--hx-card-padding, var(--hx-space-6, 1.5rem));
}

Typography:

.button {
font-family: var(--hx-button-font-family, var(--hx-font-family-sans, sans-serif));
font-weight: var(--hx-button-font-weight, var(--hx-font-weight-semibold, 600));
font-size: var(--hx-font-size-md, 1rem);
}

Borders:

.card {
border: var(--hx-border-width-thin, 1px) solid
var(--hx-card-border-color, var(--hx-color-neutral-200, #dee2e6));
border-radius: var(--hx-card-border-radius, var(--hx-border-radius-lg, 0.5rem));
}

Shadows:

.card {
box-shadow: var(--hx-card-shadow, var(--hx-shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1)));
}

Focus ring:

.button:focus-visible {
outline: var(--hx-focus-ring-width, 2px) solid
var(--hx-button-focus-ring-color, var(--hx-focus-ring-color, #2563eb));
outline-offset: var(--hx-focus-ring-offset, 2px);
}

Tokens enable powerful theming capabilities. Here are common theming patterns for consumers.

Use case: Change the primary brand color across all components.

/* Consumer's theme file */
:root {
/* Override primary color (teal instead of blue) */
--hx-color-primary-50: #e6f7f7;
--hx-color-primary-100: #cceff0;
--hx-color-primary-200: #99dfe0;
--hx-color-primary-300: #66cfd1;
--hx-color-primary-400: #33bfc1;
--hx-color-primary-500: #007878; /* Base teal */
--hx-color-primary-600: #006666;
--hx-color-primary-700: #005555;
--hx-color-primary-800: #004444;
--hx-color-primary-900: #003333;
--hx-color-primary-950: #002222;
}

Result: All components that use --hx-color-primary-* now render in teal.

Use case: Style one button differently without affecting others.

/* Hero CTA button — gradient background */
hx-button.hero-cta {
--hx-button-bg: linear-gradient(135deg, #ff6b35, #f7931e);
--hx-button-color: #ffffff;
--hx-button-border-radius: 2rem; /* Pill shape */
--hx-button-font-size: 1.25rem;
}
<hx-button class="hero-cta" variant="primary"> Get Started Free </hx-button>

Use case: Support light and dark color schemes.

/* Light mode (default) */
:root {
--hx-color-neutral-0: #ffffff;
--hx-color-neutral-50: #f8f9fa;
--hx-color-neutral-800: #343a40;
--hx-color-neutral-900: #212529;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--hx-color-neutral-0: #212529; /* Inverted */
--hx-color-neutral-50: #343a40;
--hx-color-neutral-800: #f8f9fa;
--hx-color-neutral-900: #ffffff;
}
}
/* Explicit dark theme via data attribute */
[data-theme='dark'] {
--hx-color-neutral-0: #212529;
--hx-color-neutral-50: #343a40;
--hx-color-neutral-800: #f8f9fa;
--hx-color-neutral-900: #ffffff;
}

Usage:

<!-- System preference (default) — no attribute, prefers-color-scheme drives the theme -->
<html lang="en">
</html>
<!-- Force dark theme on the document -->
<html lang="en" data-theme="dark">
</html>
<!-- Force light theme on the document -->
<html lang="en" data-theme="light">
</html>

Use case: Use a custom font family across all components.

:root {
--hx-font-family-sans: 'Inter', 'Helvetica Neue', Arial, sans-serif;
}

With web font loading:

<head>
<!-- Preload font for performance -->
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin />
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
:root {
--hx-font-family-sans: 'Inter', system-ui, sans-serif;
}
</style>
</head>

Use case: Tighter spacing for compact UIs.

:root {
--hx-space-1: 0.125rem; /* Half of default */
--hx-space-2: 0.25rem;
--hx-space-3: 0.5rem;
--hx-space-4: 0.75rem;
--hx-space-6: 1rem;
}

Use case: Support users with low vision.

@media (prefers-contrast: more) {
:root {
/* Increase border thickness */
--hx-border-width-thin: 2px;
--hx-border-width-medium: 3px;
/* Use stronger borders */
--hx-color-neutral-200: #000000;
--hx-color-neutral-300: #000000;
/* Increase focus ring visibility */
--hx-focus-ring-width: 3px;
--hx-focus-ring-offset: 3px;
}
}

Every component documents its tokens via JSDoc so they appear in Custom Elements Manifest (CEM) and IDE autocomplete.

/**
* A button component with multiple variants and sizes.
*
* @slot - Button text content
* @slot prefix - Icon or content rendered before the label
* @slot suffix - Icon or content rendered after the label
*
* @cssprop [--hx-button-bg=var(--hx-color-action-primary-bg)] - Button background color.
* @cssprop [--hx-button-color=var(--hx-color-text-on-primary)] - Button text color.
* @cssprop [--hx-button-border-color=transparent] - Button border color.
* @cssprop [--hx-button-border-radius=var(--hx-border-radius-md)] - Button corner radius.
* @cssprop [--hx-button-font-family=var(--hx-font-family-sans)] - Button font family.
* @cssprop [--hx-button-font-weight=var(--hx-font-weight-semibold)] - Button font weight.
* @cssprop [--hx-button-focus-ring-color=var(--hx-focus-ring-color)] - Focus ring color.
*
* @csspart button - The native button element.
*
* @fires hx-click - Fired when button is clicked (event forwarding from native click)
*/
@customElement('hx-button')
export class HelixButton extends HelixElement {
// Component implementation
}

hx-button does not expose --hx-button-padding-x or --hx-button-padding-y as public CSS custom properties — spacing comes from the hx-size attribute (sm / md / lg), which selects from token-driven sizing rules internally. See bundle-budgets.json for the canonical list.

/**
* @cssprop [{token-name}={default-value}] - Description of what this token controls.
*/

Rules:

  • Token name in square brackets (optional to override)
  • Default value shows the fallback chain
  • Description is concise and actionable
  • List tokens in logical order (background, text, border, spacing, typography)

/* BAD */
.button {
background: #2563eb;
padding: 0.5rem 1rem;
font-weight: 600;
border-radius: 0.375rem;
}
/* GOOD */
.button {
background: var(--hx-button-bg, var(--hx-color-primary-500, #2563eb));
padding: var(--hx-space-2, 0.5rem) var(--hx-space-4, 1rem);
font-weight: var(--hx-button-font-weight, var(--hx-font-weight-semibold, 600));
border-radius: var(--hx-button-border-radius, var(--hx-border-radius-md, 0.375rem));
}
/* BAD (missing semantic fallback) */
.card {
background: var(--hx-card-bg, #ffffff);
}
/* GOOD */
.card {
background: var(--hx-card-bg, var(--hx-color-neutral-0, #ffffff));
}
/**
* @cssprop [--hx-card-bg=var(--hx-color-neutral-0)] - Card background color.
* @cssprop [--hx-card-border-color=var(--hx-color-neutral-200)] - Card border color.
* @cssprop [--hx-card-border-radius=var(--hx-border-radius-lg)] - Card corner radius.
* @cssprop [--hx-card-padding=var(--hx-space-6)] - Card inner padding.
*/
@customElement('hx-card')
export class HxCard extends LitElement {}

All token combinations must meet at least WCAG 2.2 AA contrast — the HELiX cert posture goes further on the P0 surface (WCAG 2.2 AAA / 7:1, per aaa-verdicts.json):

  • 4.5:1 for normal text (16px and below) — WCAG 2.2 AA floor
  • 3:1 for large text (18pt+ or 14pt+ bold) — WCAG 2.2 AA floor
  • 3:1 for UI components (borders, icons) — WCAG 2.2 AA floor
  • 7:1 for normal body text on the P0 surface — WCAG 2.2 AAA
/* VERIFY: Does this meet 4.5:1? */
.button {
background: var(--hx-color-primary-500, #2563eb);
color: var(--hx-color-neutral-0, #ffffff);
}
/* Answer: Yes — #2563eb on #ffffff = 7.2:1 ratio */

Use online contrast checkers or browser DevTools to verify all color combinations.

Every component with transitions or animations respects prefers-reduced-motion:

.button {
transition: background-color var(--hx-transition-fast, 150ms ease);
}
@media (prefers-reduced-motion: reduce) {
.button {
transition: none;
}
}
/* BAD — prevents consumer customization */
:host {
background: var(--hx-card-bg, #ffffff) !important;
}
/* GOOD — allows override cascade */
:host {
background: var(--hx-card-bg, var(--hx-color-neutral-0, #ffffff));
}
/* BAD — pretends a primitive CSS variable exists (it doesn't — primitives are
build-time TS constants), so this always falls through to the raw hex. */
.button {
background: #2563eb;
}
/* GOOD — references semantic token */
.button {
background: var(--hx-button-bg, var(--hx-color-primary-500, #2563eb));
}

8. Consistent Token Usage Across Components

Section titled “8. Consistent Token Usage Across Components”

If multiple components use the same concept, they should reference the same semantic token:

/* All primary-surface components consume the same SEMANTIC action token, which
itself resolves to a primary-ramp stop (currently --hx-color-primary-700 in
the Apex default brand). Targeting the primitive directly skips the semantic
layer and bypasses brand-swap propagation. */
.button--primary {
background: var(--hx-color-action-primary-bg);
}
.badge--primary {
background: var(--hx-color-action-primary-bg);
}
.link--primary {
color: var(--hx-color-primary-500, #2563eb);
}

Design tokens are versioned independently from components using semantic versioning.

Change TypeVersionExamples
Patch (0.0.x)Bug fixesCorrecting a contrast ratio, fixing a typo in a token name
Minor (0.x.0)AdditionsNew tokens added, new variants, new component tokens
Major (x.0.0)BreakingRemoved tokens, renamed tokens, restructured hierarchy

These require a major version bump:

  • Removing a semantic token
  • Renaming a semantic token
  • Changing the structure of token naming
  • Removing a component token that consumers rely on

These do NOT require a major version bump:

  • Adding new tokens
  • Changing primitive values (consumers don’t reference primitives)
  • Adding new component tokens
  • Changing fallback values (primitives in fallback chains)

Design tokens are the foundation of HELiX’s theming architecture. They provide:

  • Consistency — All components use the same visual language
  • Flexibility — Global theming and component-level customization
  • Maintainability — Change once, apply everywhere
  • Accessibility — Contrast ratios and visual clarity enforced at the token level
  • Multi-brand support — White-label deployments through token overrides

Key takeaways:

  1. Three-tier architecture — Primitive → Semantic → Component
  2. Two-level fallback chains — Semantic token → Primitive fallback (or three-level for component tokens)
  3. Semantic tokens are the public API — Consumers override these for theming
  4. Component tokens are optional — Add them when surgical customization is needed
  5. Never hardcode values — Always use tokens with fallback chains
  6. Document all tokens — JSDoc for every CSS custom property
  7. Maintain contrast ratios — WCAG 2.2 AA minimum (4.5:1 for text, 3:1 for UI); 7:1 for body text on the P0 surface (AAA)
  8. Respect user preferences — Dark mode, high contrast, reduced motion