All guides

Design tokens

Light and dark semantic tokens: naming that scales

Semantic tokens are the difference between theming that scales and a file full of dark-mode overrides. Here is the naming and alias structure that holds up.

Published 8 min read

Most teams do not struggle to pick colors. They struggle to name them. The moment a second theme exists, every name that described a value (gray-900, brand-blue) becomes ambiguous, and every component that used one needs a conditional. Semantic tokens fix this by naming the job instead of the pigment.

The three layers, and which one you bind to

  • Primitives are the raw palette: neutral/50 through neutral/950, blue/500, red/600. They never change between themes.
  • Semantic tokens describe a role: background/default, text/secondary, border/focus. They alias primitives and are the only colors UI should reference.
  • Component tokens describe a specific component: button/primary/background. Add them only when a component needs to diverge from the semantic default, not as a first step.

Keeping all three layers in Figma is optional. Keeping the first two is not. A file with only primitives will grow dark-mode overrides; a file with only semantics cannot retheme cleanly because there is no shared source value.

Name by role, not by value

AvoidUseWhy
gray-900-texttext/primaryStays correct if text becomes near-black or a brand tint.
bg-whitebackground/defaultDark mode replaces the value, not the name.
blue-buttonaccent/defaultA rebrand changes the value once, not every component.
light-gray-borderborder/subtleDescribes hierarchy rather than a literal shade.

Use two levels: a category and an optional modifier. background/default, background/subtle, background/inverse. Slashes map cleanly to CSS custom properties and to most token formats, and they sort well in Figma's variable panel.

A core set that covers most UI

Resist the urge to enumerate every possible use. Twelve to sixteen tokens handle the majority of product screens, and a smaller set is easier to keep accessible. A practical starting point:

CategoryTokens
Backgroundbackground/default, background/subtle, background/inverse
Surfacesurface/raised, surface/overlay
Texttext/primary, text/secondary, text/muted, text/inverse
Borderborder/default, border/subtle, border/focus
Actionaccent/default, accent/hover, accent/foreground
Statusstatus/success, status/warning, status/danger, status/info

Status colors usually need a matching foreground token (status/danger plus status/danger-foreground) so text on a red banner does not depend on the page theme.

Model light and dark as modes

Create one semantic collection with a Light and a Dark mode, and define both values on the same variable. Do not create a second collection called Semantic Dark; that is how you end up with two files that diverge. In Figma, a frame or page can set the mode, so a single component can be previewed in both themes without duplication.

  • Every semantic variable should alias a primitive in both modes. A raw hex value in one mode is a future inconsistency.
  • Dark mode is not an inversion. Dark surfaces get lighter as they elevate (neutral/950 to neutral/900 to neutral/800), while light surfaces get darker as they elevate.
  • Accent colors generally move lighter in dark mode to keep contrast, and their foreground often flips to a dark neutral.
  • Borders need more contrast than you expect in dark mode; neutral/800 on neutral/950 is visible, neutral/900 usually is not.

Check contrast in both modes, not once

WCAG AA asks for 4.5:1 for normal text and 3:1 for large text and meaningful UI boundaries. A palette can pass in light mode and fail in dark mode because the same pair of shades sits at a different luminance relationship. Test the actual pairs your tokens produce: text/primary on background/default, text/secondary on background/subtle, accent/foreground on accent/default, and every status combination.

Automated adjustment is useful for finding candidates, but review the result. A token that technically reaches 4.5:1 can still look muddy or lose the intended hierarchy. See Understanding WCAG contrast minimum for the exact thresholds.

Hand off without duplicating the palette

Export tokens as Design Tokens JSON and let the build map them to CSS custom properties. The light and dark values become two selectors over the same variable names, which is the code equivalent of Figma modes.

CSS custom properties from semantic tokens
:root {
  --background-default: #fafafa;
  --text-primary: #0a0a0a;
  --accent-default: #155dfc;
}

.dark {
  --background-default: #0a0a0a;
  --text-primary: #fafafa;
  --accent-default: #51a2ff;
}

If you use Tailwind v4, reference those properties inside @theme inline so utilities follow the tokens. The result is that bg-background-default and text-text-primary switch with the theme, and components stop carrying dark: classes for color. Keep dark: for images, shadows, and cases where the layout genuinely changes.

Tailwind v4 theme bridge
@import "tailwindcss";

@theme inline {
  --color-background-default: var(--background-default);
  --color-text-primary: var(--text-primary);
  --color-accent-default: var(--accent-default);
}

When to add component tokens

Add a component token when a component needs a value that is intentionally different from the semantic default, and that difference is part of the component's contract. A destructive button that uses status/danger as its background is a reasonable component token. A card that uses background/default is not; it should inherit the semantic token directly.

  • Start with semantics only.
  • Add a component token after the second or third place that needs the same exception.
  • Name it after the component and the role: button/primary/background, not button/blue.
  • Alias semantics, never primitives, so theming still flows through one layer.

Rollout checklist

  • One semantic collection with Light and Dark modes.
  • Every variable aliases a primitive in both modes.
  • Names describe roles, not pigments or shades.
  • Contrast verified for real token pairs in both modes.
  • Tokens exported to a versioned JSON file.
  • CSS and Tailwind generated from the export, not hand-maintained.

Skip the manual setup

Generate this structure in one click

Huekit creates the color scales, semantic tokens, and Light/Dark modes inside Figma, and exports JSON tokens or a Tailwind config when you need to hand off.

Keep reading