Back to Ghost

Shade tokens, not hex

.agents/skills/shade-tokens-not-hex/SKILL.md

6.50.04.6 KB
Original Source

Shade tokens, not hex

Drive every UI colour/border/surface through semantic tokens. They flip in dark mode automatically. Hardcoded hex, hsl(...), or raw palette utilities (bg-gray-200, text-zinc-500) break dark mode and theming.

Use these instead

ConcernSemantic token (Tailwind)CSS var
Page canvasbg-background--background
One step above pagebg-surface-elevated--surface-elevated
Floating menu on top of elevatedbg-surface-elevated-2--surface-elevated-2
Primary texttext-foreground / text-primary--text-primary
Secondary / muted texttext-muted-foreground--text-secondary
Default border (cards, banners, dividers — opaque)border-border-default--border-default
Compositing border for floating surfaces (popover, dropdown, select — translucent in dark mode, usually with an opacity modifier like /60 or /30)border-border--border
Form-control border (inputs, selects, outline buttons)border-control-border--control-border
Hover (generic)bg-interactive-hover--interactive-hover
Outline button hoverbg-button-hover--button-hover
Tabs / page menu hover/activebg-tab-hover, bg-tab-active--tab-hover, --tab-active
Table row hoverbg-table-row-hover--table-row-hover
Destructivebg-destructive, text-destructive--destructive
Focus ringring-focus-ring, border-focus-ring--focus-ring

Full inventory: apps/shade/theme-variables.css — this is where the CSS custom property values live, including the .dark { ... } overrides. The companion file apps/shade/tailwind.theme.css is the Tailwind v4 @theme block that maps Tailwind utility names (bg-foreground, text-muted-foreground, etc.) to the variables defined in theme-variables.css, plus the raw --color-* scale. Edit theme-variables.css to change a token's value; edit tailwind.theme.css to wire a new utility name or add a raw colour.

Surface elevation — pick by what's behind it

  1. --background → body, app shell, editor.
  2. --surface-elevated → sidebars, cards, top bars, sticky headers.
  3. --surface-elevated-2 → floating menus (DropdownMenu, Select, Popover) and the sidebar user menu.

In light mode these all flatten to near-white; borders/shadows carry the elevation. In dark mode they're three distinct colours.

Interactive surfaces — use the dedicated tokens

Hover, active, and selected states are their own tokens — don't reach for raw greys or apply /30 opacity modifiers ad-hoc.

TokenUse for
--interactive-hoverGeneric hover: dropdown items, menu items, list rows, filter options
--button-hoverOutline / dropdown Button hover (aliased to --interactive-hover; separate so it can diverge)
--tab-hover / --tab-activeTabs (button, button-sm, pill, kpis), PageMenu, sidebar menu items, active Toggle (dark)
--table-row-hoverShade Table row hover. Also visually-table-like lists (analytics top posts, comments list, members sticky cell). Opaque, unlike the other hover tokens

Form-control border

Inputs, textareas, outline Buttons, dropdown triggers, and the inputSurface recipe use --control-border, not --border-default. In dark mode it lifts a step above the page border so form controls keep contrast against the page surface. Borders inside cards or popovers can keep using --border-default.

Correct

tsx
<div className="bg-surface-elevated border border-border-default rounded-md p-4">
    <p className="text-foreground">Primary copy</p>
    <p className="text-sm text-muted-foreground">Secondary copy</p>
</div>

Incorrect

tsx
// BAD — raw palette utilities
<div className="bg-gray-50 dark:bg-gray-900 border border-gray-200">

// BAD — hex values
<div style={{ backgroundColor: '#f9fafb', color: '#111' }}>

// BAD — wrapping a token in hsl() (it already contains hsl())
<div style={{ background: 'hsl(var(--background))' }}>

// BAD — opacity-modifier-on-raw-grey instead of the hover token
<div className="hover:bg-gray-900/30">

Inside CSS

css
/* Correct — variable already contains hsl(...) */
.thing { background: var(--surface-elevated); }

/* Incorrect — double-wraps */
.thing { background: hsl(var(--surface-elevated)); }

When no semantic token fits

Use a raw token from apps/shade/tailwind.theme.css (chart series, brand assets, illustrations) — never a literal hex. If you need a new colour, add it to theme-variables.css (semantic) or tailwind.theme.css (raw @theme). Don't introduce ad-hoc CSS variables in component files.