Skip to content

Semantic tokens

Semantic tokens name why a value exists rather than what raw value it currently resolves to. A component asks for a default surface, primary text, or control radius; the active theme chooses the appropriate primitive behind that role.

  • Primitive — aidsgn.color.blue.500: stores a raw palette value with no component intent.
  • Semantic — aidsgn.color.surface.action: names a UI purpose and references one primitive.
  • Theme-mode override — aidsgn.dark.color.surface.default: replaces an existing semantic path with another primitive for one mode.
  • Brand override — aidsgn.offprint.light.color.surface.default: maps the same role to a Brand-owned primitive.

The Style Dictionary formatter determines the layer from the source directory. It rejects semantic or theme values that are not direct, type-compatible primitive references, and rejects theme paths without a matching base semantic token. Generated CSS keeps aliases visible:

/* Primitive tokens: raw palettes and scales for aliases and internal layout. */
--aidsgn-color-blue-500: oklch(52% 0.21 260);
/* Semantic tokens: the public component styling contract. */
--aidsgn-color-surface-action: var(--aidsgn-color-blue-500);

Components consume semantic variables for public visual intent:

  • --aidsgn-color-* roles cover text, links, surfaces, borders, focus, and status treatments.
  • --aidsgn-control-* roles coordinate control padding and shape.
  • --aidsgn-selection-control-* roles separate the themeable 20px visible Checkbox/Radio indicator from the invariant 36px minimum interactive target.
  • --aidsgn-typography-* roles provide complete type recipes.
  • Component roles such as --aidsgn-checkbox-radius, --aidsgn-tooltip-surface, and --aidsgn-modal-motion-* capture justified component-specific meaning.
.control {
color: var(--aidsgn-color-text-primary);
background: var(--aidsgn-color-surface-default);
border-radius: var(--aidsgn-control-radius);
}

Light mode is the fallback, and an unconfigured page automatically follows prefers-color-scheme: dark. Set data-aidsgn-theme="dark" or data-aidsgn-theme="light" on the root element for an explicit override. Components keep the same declarations in both modes.

Offprint is the first alternate Brand. Import its opt-in bundle after the base tokens, then select it independently from Theme mode:

@import "@aidsgn/tokens/css";
@import "@aidsgn/tokens/brands/offprint.css";
<html data-aidsgn-brand="offprint" data-aidsgn-theme="light"></html>

Offprint maps shared semantic roles to warm paper, ink, vermilion, square geometry, and editorial typography without changing component implementations. Primary actions use ink while persistent selected/current states use the separate color.surface.selected, color.text.selected, and color.text.on-selected roles. Omit data-aidsgn-brand for aidsgn; omit data-aidsgn-theme to keep either Brand on automatic system mode. Use the Brand and Theme-mode toolbars in any Storybook for a live comparison.

Use semantic tokens in component styles and application UI whenever the value expresses intent. Use primitives to author semantic aliases, inspect the raw scale, or handle internal layout where no semantic contract exists. If several components independently need the same intentional value, promote that relationship into a semantic token instead of copying a primitive reference.

Ask these questions in order:

  1. What role does the value play: text, surface, border, focus, status, control shape, typography, or elevation?
  2. Should that role change consistently across themes or components? If yes, use an existing global semantic token.
  3. Is the relationship unique and durable for one component? If yes, use a component semantic token.
  4. Is it only local geometry with no theme meaning? A primitive spacing token is appropriate.
In a component Choose Avoid
Primary Button surface and label color.surface.action with color.text.inverse Direct blue and white palette steps
Input text, placeholder, and border color.text.primary, color.text.placeholder, and color.border.default; invalid fields use color.text.error for the error-colored boundary and message One neutral chosen for every text state or a direct red palette step
Alert info/success/warning/error variants The matching semantic surface and text status pair with color.border.default Color alone or unrelated palette steps that drift between themes
Card surface and separation color.surface.default, color.border.default, and shadow.sm A Card-only token when global roles already express the intent
Switch thumb switch.thumb surface.default, because the thumb has to stay light on both inactive and active tracks
Selection control geometry selection-control.* and switch.*-size Repeated literals that blur the visible-indicator and interactive-target roles
Tooltip tooltip.surface, tooltip.text, and tooltip.radius Default surface/text roles, because Tooltip intentionally contrasts with its surroundings
Internal layout gap A space.* primitive A new semantic token used by only one incidental layout
.aidsgn-button--primary {
background: var(--aidsgn-color-surface-action, #175cd3);
color: var(--aidsgn-color-text-inverse, #fff);
}
.aidsgn-input[aria-invalid="true"] {
border-color: var(--aidsgn-color-text-error, #b42318);
}

The variable is the design-system contract. The literal is only a resilience fallback; never choose the fallback first and then search for a similar token. Direct palette variables such as --aidsgn-color-blue-500 do not belong in component visual contracts because themes and future brands may map the same semantic role to a different primitive.

Tailwind v4 follows the same boundary. Utilities such as bg-aidsgn-surface-action, text-aidsgn-text-error, and border-aidsgn-border-focus map to semantic variables, while primitive Brand colors are intentionally not exported as utilities. See the Tailwind CSS v4 integration for the complete naming and runtime behavior.

Add a global semantic token when multiple components need the same named intent. Add a component token when one component has a stable relationship that global roles cannot describe, as with the Switch thumb or inverted Tooltip surface. Keep a primitive in local component layout when it merely supplies spacing and does not need to vary by theme.

Before adding anything, write the proposed role as a sentence. If the description only says what the value looks like (for example, “medium blue”), it is still a primitive. A semantic description says where and why it is used, including pairing or accessibility constraints.