Skip to content

Semantic Tokens

Semantic tokens add meaning to your primitives. Instead of saying “use shade 500 of the primary palette,” a semantic token says “use the primary text color.” This layer of indirection is what enables theming — the same semantic token name resolves to different primitive values in light and dark mode.

Select Semantic in the sidebar to see all 5 categories.


Every semantic token is an alias that references a primitive. You don’t enter raw hex codes or pixel values — you select which primitive token it should point to.

semantic-color-text-primary-default
Light mode → primitive-color-brand-primary-700 (dark shade for contrast on light bg)
Dark mode → primitive-color-brand-primary-300 (light shade for contrast on dark bg)

This reference structure means:

  1. Changing a primitive automatically updates all semantics that reference it
  2. Light and dark themes use the same semantic names but different primitive references
  3. Your UI code always uses semantic names, never raw values

The largest semantic category. Semantic colors map your primitive palettes to purpose-driven roles with full light/dark theme support.

There are 8 color intents, each representing a different purpose:

IntentPurposeTypical source palette
PrimaryMain brand actions and emphasisBrand Primary
SecondarySupporting actions and contentBrand Secondary
AccentHighlights and decorative elementsBrand Accent
NeutralDefault text, borders, surfacesNeutral Gray
SuccessPositive feedback, confirmationsFeedback Success
WarningCaution states, alertsFeedback Warning
ErrorError states, destructive actionsFeedback Error
InfoInformational content, hintsFeedback Info

Each intent is available across four roles:

RoleWhat it colorsExamples
TextForeground textHeadings, labels, body copy, links
SurfaceBackgroundsCards, panels, page backgrounds, hover states
BorderOutlines and dividersCard borders, input outlines, separators
InteractiveActionable elementsButton backgrounds, link colors, focus rings

Each role has multiple variants for different emphasis levels:

  • default — Standard usage
  • subtle — Lower emphasis (lighter backgrounds, muted text)
  • bold — Higher emphasis (darker backgrounds, prominent text)
  • contrast — (Semantic text only) Used for text on dark surfaces (e.g., white text on a bold background)

This creates a matrix of approximately 80+ semantic color tokens (8 intents x 4 roles x 2-4 variants each).

The semantic color editor shows side-by-side editing for light and dark modes. Each token has:

  • A sun icon column for the light mode value
  • A moon icon column for the dark mode value

For each mode, you select which primitive shade the semantic token should reference using a dropdown. The dropdown lists all available shades from the relevant primitive palette.

For example, semantic-color-text-primary-default:

  • Light mode dropdown: select from brand-primary shades (50-950) — typically a dark shade like 700 for good contrast on light backgrounds
  • Dark mode dropdown: select from brand-primary shades (50-950) — typically a light shade like 300 for good contrast on dark backgrounds
/* Light mode */
[data-theme="light"] {
--semantic-color-text-primary-default: var(--primitive-color-brand-primary-700);
--semantic-color-surface-neutral-subtle: var(--primitive-color-neutral-gray-50);
--semantic-color-border-neutral-default: var(--primitive-color-neutral-gray-300);
}
/* Dark mode */
[data-theme="dark"] {
--semantic-color-text-primary-default: var(--primitive-color-brand-primary-300);
--semantic-color-surface-neutral-subtle: var(--primitive-color-neutral-gray-900);
--semantic-color-border-neutral-default: var(--primitive-color-neutral-gray-600);
}

Semantic typography defines composite type styles — combinations of font family, size, weight, line height, letter spacing, and text transform that form a complete text treatment.

Typography semantic tokens are organized into style categories, each accessible via tabs:

CategoryTypical use
DisplayHero sections, landing pages, feature highlights
HeadingsHeading levels (H1–H6)
BodyStandard body and paragraph text
LabelsForm labels, UI labels
CaptionsSmall supporting text
CodeMonospace, code snippets
OverlineUppercase small text above headings or sections

Each style token includes a live preview and configurable properties:

PropertySelects from
Font familyPrimitive typography families (display, heading, body, mono)
Font sizePrimitive typography size scale
Font weightPrimitive typography weights
Line heightPrimitive typography line heights
Letter spacingPrimitive typography letter spacing
Text transformnone, uppercase, lowercase, capitalize

Properties marked (Inherited) pull their values from your primitive typography settings. You can override any inherited value at the semantic level. By default, most properties are set to inherit — you only need to override the specific properties you want to change.

Type styles export as composite token objects in JSON:

{
"semantic": {
"typography": {
"heading": {
"lg": {
"fontFamily": "{primitive.typography.family.heading}",
"fontSize": "{primitive.typography.size.2xl}",
"fontWeight": "{primitive.typography.weight.bold}",
"lineHeight": "{primitive.typography.lineheight.tight}",
"letterSpacing": "{primitive.typography.letterspacing.tight}"
}
}
}
}
}

Semantic spacing creates purpose-driven aliases for your primitive spacing scale. While primitives define what sizes exist (xs, sm, md, lg…), semantic spacing defines how those sizes are used.

CategoryPurposeCSS analogy
StackVertical spacing between elementsmargin-bottom, gap (column)
InsetInternal padding of containerspadding
InlineHorizontal spacing between elementsmargin-right, gap (row)

Each category has sizes from xs through xl (or similar), and each size maps to a primitive spacing token via a dropdown selector.

You could use --primitive-spacing-md everywhere. But semantic spacing lets you:

  1. Differentiate usage — Stack-md and Inset-md can map to different primitive values (e.g., vertical rhythm might use larger spacing than padding)
  2. Change proportions globally — Want tighter padding everywhere? Change the Inset mappings without touching Stack or Inline
  3. Communicate intentsemantic-spacing-stack-md clearly says “medium vertical spacing between blocks”

Each spacing token shows:

  • The current mapping (e.g., ”→ primitive-spacing-lg”)
  • The resolved pixel value
  • A dropdown to change which primitive it references
--semantic-spacing-stack-sm: var(--primitive-spacing-sm);
--semantic-spacing-stack-md: var(--primitive-spacing-md);
--semantic-spacing-inset-md: var(--primitive-spacing-lg);
--semantic-spacing-inline-sm: var(--primitive-spacing-sm);

Focus tokens control the appearance of keyboard focus indicators — essential for accessibility. When users navigate with a keyboard (Tab key), focused elements need a clearly visible outline.

Define focus ring styles for keyboard navigation and accessibility. These tokens control the outline and ring appearance when users tab through interactive elements. A live preview shows your current configuration.

The focus ring is composed of two layers — an outline (inner) and a ring (outer) — giving you fine-grained control over the visual treatment.

PropertyControlsDefault
Outline widthThickness of the inner outlineborder-width.medium (2px)
Outline offsetGap between the element and the outlinespacing.xs (2px)
Outline colorColor of the inner outlinePrimary Default
Ring widthThickness of the outer ringborder-width.heavy (3px)
Ring colorColor of the outer ringPrimary Default
Ring opacityTransparency of the outer ringopacity.alpha-25 (25%)

Each property references an existing primitive or semantic token, keeping your focus styles connected to the rest of your system.

Color properties use a grouped dropdown with options organized by:

  • Intent variants — Colors from each semantic intent (primary, secondary, neutral, etc.)
  • Other — Additional color options

This lets you pick a focus color that’s consistent with your brand but distinct enough to be visible.

Focus rings are critical for accessibility compliance (WCAG 2.4.7). The default two-layer approach — a solid outline with a semi-transparent outer ring — provides strong visibility across different background colors. If you customize these values, test with keyboard navigation to ensure focus remains clearly visible throughout your design system.

--semantic-focus-ringwidth: 2px;
--semantic-focus-ringcolor: var(--semantic-color-border-primary-default);
--semantic-focus-outlineoffset: 2px;

Semantic transitions define named transition presets that combine a duration and easing curve into a reusable animation style.

PresetTypical use
SlowPage transitions, layout shifts
BaseStandard UI transitions (dropdowns, panels)
FastMicro-interactions (hover, focus, button press)
ColorColor-only transitions (background, text color changes)

Each transition has:

PropertySelects from
DurationPrimitive duration tokens (instant, fast, base, moderate, slow, slower)
EasingPrimitive easing tokens (standard, decelerate, accelerate, bounce, etc.)
DescriptionFree-text description of when to use this transition

Each transition card includes a play button that triggers an animation preview. A colored block animates across the card using the configured duration and easing, so you can visually evaluate how the transition feels before using it.

Transitions export as composite tokens in JSON, referencing their primitive components:

{
"semantic": {
"transition": {
"fast": {
"duration": "{primitive.duration.fast}",
"easing": "{primitive.easing.standard}"
}
}
}
}

Why can’t I enter a hex value directly in semantic color?

Section titled “Why can’t I enter a hex value directly in semantic color?”

By design. Semantic tokens are aliases that reference primitives — they don’t hold raw values. This ensures the layer architecture works correctly: change a primitive once, and it cascades through all semantics that reference it.

How does dark mode work at the semantic level?

Section titled “How does dark mode work at the semantic level?”

Each semantic color token has two mappings — one for light mode and one for dark mode. Both map to primitive shades, but typically use opposite ends of the shade scale. Light mode text uses dark shades (700-900) for contrast on light backgrounds; dark mode text uses light shades (100-400) for contrast on dark backgrounds. The [data-theme] attribute on your HTML element controls which mapping is active.

What if a type style property is left empty?

Section titled “What if a type style property is left empty?”

Empty (inherited) properties are set by the primitive typography defaults. This is useful when you want a type style to follow the base configuration for most properties but override just one or two (e.g., set a different font weight for headings but inherit everything else).

Not in the current version. The five semantic categories (Color, Typography, Spacing, Focus, Transitions) are fixed. You can customize all values within these categories, but you can’t add new categories like “Semantic Shadow” or “Semantic Radius.”

Should I map every semantic size to a different primitive?

Section titled “Should I map every semantic size to a different primitive?”

Not necessarily. It’s fine for stack-md and inset-md to reference the same primitive (spacing-md). The semantic layer exists for when you want them to differ, not as a requirement that they must.