Semantic color roles
Naming a color by the job it does (primary, surface, danger) instead of what it looks like, so themes swap without touching components.
See it
What it is
Semantic roles name a color by the job it does, not by the hue it happens to be. Instead of 'blue-600' scattered through the code, you get 'primary', 'surface', 'muted', 'border', 'destructive', each pointing at a step from your color scale. Material 3 formalized this with roles like primary/on-primary and surface/on-surface; shadcn and Radix ship a leaner set built on background and foreground pairs.
The payoff is swapping. Dark mode, a second brand, a seasonal theme, or a client rebrand becomes a change of what each role points to, with zero edits in components. Roles also carry meaning that raw hues cannot: 'destructive' tells the next developer that this button deletes something, while 'red-500' just tells them it is red. Pair roles so every background role has a matching foreground role guaranteed to be readable on it.
Gotcha: the abstraction leaks the moment you name a role after its current value ('primary-blue', 'gray-surface') or invent one role per component ('sidebar-hover-alt-2'). Keep the set small enough to hold in your head, and expect the honest fight to be dark mode, where you often need a different scale step per theme rather than one clever inversion.
Ask AI for it
Refactor this color system into semantic roles. Define CSS custom properties for background, foreground, card, muted, muted-foreground, border, input, ring, primary, primary-foreground, secondary, accent, destructive, and destructive-foreground, each mapped to a step from the existing tonal ramps. Provide a light and a dark theme by remapping only those variables, guarantee every foreground role clears 4.5:1 on its paired background, and replace all raw hex values in components with the role variables.