tailwind token system

why

arbitrary values scattered through utilities make the visual system unownable: a theme change means hunting every literal color and radius across the codebase, and drift creeps in wherever a value was copied rather than named. semantic tokens keep one owner for every visual role, so theming, rebranding, and consistency become a token-file edit instead of a global search-and-replace.

rule

Tailwind utilities express layout and composition; semantic tokens express the visual system. a component selects a semantic token, never an arbitrary color or radius value that only makes sense in one place.

ownership

  • shared CSS owns semantic variables and Tailwind mappings.
  • components own their presentation through declared variants.
  • the innermost wrapper containing final information or controls owns padding.

separate tokens from composition

tokens answer "what visual role is this?"; utilities answer "how is this instance laid out?" a surface token can change across themes while a grid, stack, or alignment utility remains local composition. this separation makes a visual correction flow to every semantic consumer without turning a component into a global CSS exception.

concernownerexample decision
canvas, surface, text, border, action, destructive rolesshared token sourceone semantic variable per role, with light/dark values where supported.
component intent, density, interaction statecomponent variantmap named variants to tokens and utilities.
local layout, order, gap, responsive arrangementconsuming compositionchoose Tailwind utilities on the parent layout/wrapper; do not override an authored child's internal classes.
final content/control insetinnermost meaningful wrapperown padding once; decorative wrappers do not add a second inset.

decision process

shortcutfailureselected direction
use a palette value in a componenta later theme change cannot find the semantic consumeruse the role token.
add a global selector for one exceptional componentstyle ownership becomes non-local and cascade-dependentadd a component variant only if callers share a named intent.
let every wrapper add spacingnested cards acquire accidental density and uneven hit areasdecide the final content wrapper that owns padding.
create a new token for a one-off visual adjustmentthe token system becomes a list of exceptionskeep genuinely local composition local.

verification

inspect a representative surface in every supported theme and interaction state. a role change should be traceable from token to component variant to rendered element; no raw palette or one-off global selector should be required to explain its appearance. manually change one semantic token value and confirm it updates every consuming component; a component that does not change is using raw palette or one-off CSS instead of the semantic role.

do not

  • stack padding across decorative wrappers.
  • use raw palette colors where a semantic token exists.
  • add one-off global CSS to change a component instance.

good and bad

bad

// Card.tsx
<div className="bg-slate-500 text-slate-50 p-4">Content</div>
// StatusBadge.tsx
<span className="bg-slate-500 px-2 py-1">Active</span>
// ErrorAlert.tsx
<div className="bg-slate-500 text-white">Error message</div>

three components use the same palette value for different semantic purposes: content surface, status indicator, error container. when the audit says "success should use emerald, error should use red," you must find every hardcoded slate-500, infer its intent, and decide case by case. a new callout copies bg-slate-500 from three places, making the fourth use undefined. a theme refresh changes --slate-500 for contrast, but only Card updates correctly; the others happen to look right anyway. audit finds success green in one place, accidental teal in another, broken red in a third. inconsistency becomes unfixable: you don't know which uses are intentional.

good

// tokens.css
:root {
--surface: hsl(0 0% 98%); /* background layers and containers */
--foreground: hsl(0 0% 10%); /* primary and secondary text */
--success: hsl(142 71% 45%); /* positive status and actionable confirmation */
--error: hsl(0 84% 60%); /* destructive and error states */
}
// Card.tsx — owns its purpose
<div className="bg-surface text-foreground p-4">Content</div>
// StatusBadge.tsx — owns its semantic role
<span className="bg-success text-foreground px-2 py-1">Active</span>
// ErrorAlert.tsx — owns error semantics
<div className="bg-error text-surface">Error message</div>

each component selects its semantic role. a design audit changes --success once and all status components update; a new status component reads the shared tokens and picks bg-success with confidence; a theme refresh changes error color once and all error states update together. visual semantics stay consistent because named tokens own them, not scattered palette copies.

search pages

go to any page