Laurel

Color

Neutrals do the work; sage marks the moments. Two OKLCH ramps, in light and dark. Contrast comes from lightness, not from saturation.

Neutrals do the work, sage marks the moments. Text, surfaces and borders come from a neutral ramp — pure white in light, a neutral charcoal in dark, never green-tinted. Sage (--primary, the org's deep green) is reserved for primary fills, selected and active states, focus rings and occasional brand moments; it never carries body or muted text.

Both ramps — --neutral-* and --sage-* — are defined as OKLCH values in theme.css, each with its hue locked, under :root, .light (light) and .dark. Semantic variables (--background, --primary, --muted-foreground, …) map onto ramp steps, so retuning a ramp retunes every component that reads it. /evaluate lets you try a new ramp anchor — hue, chroma, lightness — against real screens before promoting it here.

--sage-*
50
100
200
300
400
500
600
700
800
900
950
--neutral-* (850 is an extra dark-surface step)
50
100
200
300
400
500
600
700
800
850
900
950

Roles

Every swatch below is painted from its CSS variable — change a value in theme.css and this page changes with it. Each panel is forced to its theme (.light / .dark), so both are visible whatever theme you are reading in.

Light · :root
--background
Page ground
--foreground
Body text
--card
Cards and inputs
--popover
Popovers, menus, dialogs
--primary
Primary actions, focus, marks
--primary-foreground
Text on a primary fill
--secondary
Secondary fills
--muted
Subtle fills
--muted-foreground
Quiet text and icons
--accent
Hover and selected fills
--destructive
Destructive actions and errors
--border
Dividers — a faint hairline
--input
Form-control boundary — a stronger step, for WCAG 1.4.11
--ring
Focus ring
Dark · .dark
--background
Page ground
--foreground
Body text
--card
Cards and inputs
--popover
Popovers, menus, dialogs
--primary
Primary actions, focus, marks
--primary-foreground
Text on a primary fill
--secondary
Secondary fills
--muted
Subtle fills
--muted-foreground
Quiet text and icons
--accent
Hover and selected fills
--destructive
Destructive actions and errors
--border
Dividers — a faint hairline
--input
Form-control boundary — a stronger step, for WCAG 1.4.11
--ring
Focus ring

Hover

Buttons hover to an explicit token, not an opacity tint (bg-primary/80 and similar). --primary-hover and --secondary-hover are a ramp step darker in light and mostly a step lighter in dark — "mostly," because the full step drops white button text below 4.5:1 in dark, so it's capped short of it. --destructive-hover isn't a ramp step (destructive has no multi-step ramp); it's interpolated toward the solid --destructive, capped the same way. npm run check:contrast guards all three.

Light · :root
base
hover
Primary
--primary → --primary-hover
base
hover
Secondary
--secondary → --secondary-hover
base
hover
Destructive
--destructive-subtle → --destructive-hover
Dark · .dark
base
hover
Primary
--primary → --primary-hover
base
hover
Secondary
--secondary → --secondary-hover
base
hover
Destructive
--destructive-subtle → --destructive-hover

Status colours

--success, --warning, --info and --destructive are semantic, not decorative: each has a solid fill, a -foreground for text on that fill, a -subtle background and a -border. They sit on their own hue — success is emerald, not sage — so the brand accent never doubles as "success," and a status colour never reads as a brand moment. Reach for one of these when a state means something (an alert, a badge, a table cell), and reach for --primary only when it means "this is the product's own action or emphasis."

Light · :root
SuccessSuccess
--success
--success-foreground
--success-subtle
--success-border
WarningWarning
--warning
--warning-foreground
--warning-subtle
--warning-border
InfoInfo
--info
--info-foreground
--info-subtle
--info-border
DestructiveDestructive
--destructive
--destructive-foreground
--destructive-subtle
--destructive-border
Dark · .dark
SuccessSuccess
--success
--success-foreground
--success-subtle
--success-border
WarningWarning
--warning
--warning-foreground
--warning-subtle
--warning-border
InfoInfo
--info
--info-foreground
--info-subtle
--info-border
DestructiveDestructive
--destructive
--destructive-foreground
--destructive-subtle
--destructive-border

Charts

The five chart colours are a run of the primary hue from light to dark. In dark the run is shifted lighter so every step stays visible on the deep ground.

Light · :root
1--chart-1
2--chart-2
3--chart-3
4--chart-4
5--chart-5
Dark · .dark
1--chart-1
2--chart-2
3--chart-3
4--chart-4
5--chart-5

Dark principles

  • A neutral ground, not black. The ground is a very dark neutral charcoal, not tinted toward the brand hue. Pure black is as off-limits as pure white.
  • Elevation is lighter. Cards and popovers sit one step above the ground, and a hairline edge (--border) defines them. Shadows only deepen.
  • The primary stays put. --primary moves to the sage ramp's mid-tone, not a lifted pastel — the same hue as light, so a primary button, a link and a focus ring read as the same brand in both themes.
  • Dividers are hairlines of the neutral ramp; form-control boundaries (--input) are a separate, stronger step, tuned to clear WCAG 1.4.11 (3:1) — dividers don't need to.
  • Off-white, neutral text. Never #fff.
  • Destructive lightens to stay legible on the dark ground.

The whole set is guarded by npm run check:contrast, which asserts the WCAG ratios for both themes — see Dark mode.

In shadcn

You do not reach for these variables by hand in component code. Write bg-primary, text-muted-foreground, border-border and the values follow — in both themes. See the theme variables.

<div className="rounded-md border bg-card p-4 text-card-foreground">
  <p className="text-muted-foreground">Muted, from --muted-foreground.</p>
</div>

On this page