Skip to content

Neutral ramps

The data-neutral attribute on <html> (or any subtree) swaps the neutral ramp — the greys behind every surface, border, muted background, secondary button, and body text — without touching the accent colour. Five ramps ship out of the box, each tuned for both light and dark.

data-neutral is orthogonal to data-color (accent), data-theme (light / dark), and data-density. Set them on the same ancestor and they all cascade together — e.g. an indigo accent on a slate neutral in dark mode is data-color="indigo" data-neutral="slate" data-theme="dark".

data-neutral valueCharacterLight page bg / borderDark surface
(none) / grayDefault neutral#f9fafb / #d0d5dd#1f2937
slateCool, blue-tinted#f8fafc / #cbd5e1#1e293b
zincCool, desaturated#fafafa / #d4d4d8#27272a
neutralPure grey#fafafa / #d4d4d4#262626
stoneWarm, brown-tinted#fafaf9 / #d6d3d1#292524

In light mode the card surface stays white for every ramp (only the page background, borders, muted fills, secondary buttons, and text shift) — matching the default. In dark mode the surface itself re-tints to the ramp’s 800 shade. Toggle the theme switch in the header to see the dark variant of each row below.

Each panel below carries a different data-neutral value. The outer panel uses the ramp’s page background; the inner card is a surface.

gray
Card surface

Muted helper text on this ramp.

slate
Card surface

Muted helper text on this ramp.

zinc
Card surface

Muted helper text on this ramp.

neutral
Card surface

Muted helper text on this ramp.

stone
Card surface

Muted helper text on this ramp.

Site-wide, set it on <html>:

<html data-neutral="slate"></html>

Per-section override:

<section data-neutral="stone"></section>

Like the other axes, it works on a subtree because each ramp block re-declares the affected component variables with concrete values (no frozen var() inheritance) — see how the cascade works.

Each non-default ramp overrides these semantic keys (light in neutral.<ramp>.tokens.json, dark in neutral.<ramp>.dark.tokens.json):

--hc-color-bg --hc-color-text --hc-color-text-muted
--hc-color-border --hc-color-muted-bg --hc-color-surface (dark only)
--hc-color-action-secondary-* --hc-color-status-neutral-*

These cascade into every component’s --hc-{component}-* surface / text / border variables. The build re-emits the affected leaves inside [data-neutral="<ramp>"] (light) and [data-theme="dark"][data-neutral="<ramp>"] (dark) blocks.

Deliberately not affected:

  • Accent (action.primary, focus ring) — owned by data-color.
  • Semantic info / success / warning / error — fixed status palette.
  • Control sizing — owned by data-density.
  • Color themes — the accent axis. Pair it with a neutral.
  • Theme builder — its Full theme mode bakes an accent + neutral together.
  • Color palette — the primitive ramps these resolve to.
  • Density — orthogonal sizing axis.