Semantic tokens, not hex

Style surfaces with tokens like bg-card, never hex or palette classes.

shadcn/uiui.shadcn.com
Theming
DocsRead docs ↗

From shadcn: Theming, and palette classes as an anti-pattern.

Edit a token, see who follows

Pick a token, then drag its sliders.

HardcodedBad

Invite teammate

They get access to this workspace.

3 seats left
bg-neutral-900border-neutral-800bg-[#1447e6]
TokensGood

Invite teammate

They get access to this workspace.

3 seats left
bg-cardborder-borderbg-primary

Both panes match.

/* globals.css */
@theme inline {
  --color-background: oklch(0.145 0 0);
  --color-card: oklch(0.205 0 0);
  --color-primary: oklch(0.488 0.243 264.376);
  --color-ring: oklch(0.488 0.243 264.376);
  --color-border: oklch(0.269 0 0);
  --color-input: oklch(0.269 0 0);
}

<Card className="bg-card text-card-foreground border-border">
  <Button className="bg-primary text-primary-foreground" />
</Card>

Swap the base palette

Pick a base color and a mode.

Hex + palette classesBad
Overview
Revenue$48.2k
Open12
Overdue3
Acme Inc.Paid
GlobexDue
bg-neutral-950text-neutral-400bg-blue-700
Semantic tokensGood
Overview
Revenue$48.2k
Open12
Overdue3
Acme Inc.Paid
GlobexDue
bg-backgroundtext-muted-foregroundbg-primary

neutral, dark

/* components.json: "baseColor": "neutral" */
.dark {
  --background: oklch(0.145 0 0);
  --card: oklch(0.205 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --border: oklch(0.269 0 0);
}
@theme inline {
  --color-background: var(--background);
  --color-card: var(--card);
  --color-muted-foreground: var(--muted-foreground);
  --color-border: var(--border);
}

Find the drift

Hover or tap a part, then switch to light.

Settings card3 of 8 hardcoded

Workspace

Shown on invoices and emails.

-

Dark mode hides the drift.

<Card className="bg-card border-border">
  <CardTitle className="text-card-foreground">Workspace</CardTitle>
  <CardDescription className="text-muted-foreground">
    Shown on invoices and emails.
  </CardDescription>
  <Label className="text-foreground">Name</Label>
  <Input className="border-input focus-visible:ring-ring" />
  <Button>Save</Button>           {/* bg-primary text-primary-foreground */}
  <Button variant="secondary">Cancel</Button>
</Card>

Why it matters, pros and cons

Why it matters

A token names a role, not a color; change it once and every surface follows.

Pros

  • Dark mode is one CSS block.
  • Surface and foreground stay paired.
  • Hex on a surface is greppable.

Cons

  • bg-card hides the color; check devtools.
  • Token sprawl: add tokens for roles, not elements.
  • Brand marks and data-viz hues should not re-theme.