Color tokens
elements man style/tokens/color Read as markdownColor is defined in two steps: raw OKLCH ramps, then semantic tokens that point
at them. App CSS reads the semantic tokens. Everything here lives in
themes/default.css inside @layer tokens.
Why OKLCH
The ramps are authored in OKLCH so that the same numbered step reads at the same
perceived weight across every ramp. --danger-500 and --success-500 sit at
the same lightness, so a red badge and a green badge feel like the same object
in different colors. A couple of brand-exact anchors are pinned as hex
(--neutral-900 is the Elements navy #102C42 exactly).
The neutral ramp
A single cool, navy-tinted gray ramp (hue 240, low chroma), from --neutral-50
(near-white) to --neutral-950 (near-black), plus --white and --black.
--neutral-900 is pinned to #102C42.
--white --black
--neutral-50 --neutral-100 --neutral-200 --neutral-300 --neutral-400
--neutral-500 --neutral-600 --neutral-700 --neutral-800 --neutral-900 --neutral-950
The status ramps
Four ramps, each a full 50–950 scale on the same lightness curve as the neutral ramp:
--success-*: green, hue 150--warning-*: amber, hue 75--danger-*: red, hue 25--info-*: cyan, hue 200 (deliberately distinct from a typical blue accent)
Semantic tokens
These are what you consume. Each flips automatically in dark mode.
Surfaces (--bg*), page and panel backgrounds:
| Token | Role |
|---|---|
--bg |
Page background (white in light) |
--bg-soft |
Slightly raised surface |
--bg-muted |
Muted fill (code, chips) |
--bg-panel |
Panel background |
--bg-elevated |
Elevated surface (cards, popovers) |
--bg-canvas |
Recessed canvas behind panels |
--bg-inverse |
Inverted surface (<pre> blocks) |
Ink (--ink*), text colors, high to low emphasis:
| Token | Role |
|---|---|
--ink |
Primary text |
--ink-soft |
Secondary text |
--ink-dim |
Tertiary / captions |
--ink-faint |
Placeholder-level |
--ink-inverse |
Text on an inverse surface |
Rules (--rule*), border colors:
| Token | Role |
|---|---|
--rule |
Default border |
--rule-soft |
Faint divider |
--rule-strong |
Emphasized border |
Accent (--accent*), the brand axis, monochrome by default:
| Token | Role |
|---|---|
--accent |
The brand color (defaults to --neutral-900) |
--accent-deep |
Hover / pressed / gradient end |
--accent-soft |
Tinted accent backgrounds |
--accent-ink |
Text/icons on an accent fill |
--accent-edge |
Accent border on tinted surfaces |
--accent points at --neutral-900 by default, so an unbranded app is
black-and-white. Repointing this block is the entire branding API. See
branding.
Status triples
Every status ramp collapses to a four-token set built for alerts, badges, and toasts (a soft background, an ink color, a solid color, and an edge):
.alert {
background: var(--success-soft);
color: var(--success-ink);
border: 1px solid var(--success);
}
The set exists for each status:
| Solid | Soft bg | Ink | Edge |
|---|---|---|---|
--success |
--success-soft |
--success-ink |
--success-edge |
--warning |
--warning-soft |
--warning-ink |
--warning-edge |
--danger |
--danger-soft |
--danger-ink |
--danger-edge |
--info |
--info-soft |
--info-ink |
--info-edge |
The accent axis follows the same shape (--accent / --accent-soft /
--accent-ink / --accent-edge), which is why callouts and pills have an
is-accent variant alongside the status ones.
Note: the solid status colors are tuned for text-on-tint and borders, where the
ink color is the readable foreground. A filled danger button is a separate
case. It sets color: var(--white) on a --danger fill deliberately, rather
than using --danger-ink (which in dark mode is a light text tint, not a fill
foreground). Don't assume --{status}-ink is always a valid text color on a
solid --{status} fill.
Focus
Focus is one ring spec, consumed by every focusable component:
| Token | Role |
|---|---|
--focus |
The ring color (defaults to --accent) |
--focus-ring |
The full box-shadow ring |
--focus-ring-danger |
The ring for destructive controls |
:focus-visible { box-shadow: var(--focus-ring); }
Dark mode
themes/default.css remaps every semantic token above under @media (prefers-color-scheme: dark) and again under :root[data-theme="dark"]. The
ramps don't move; the semantic pointers do (--bg → --neutral-950, --ink
→ --neutral-50, and so on). Anything built from semantic tokens is
dark-ready for free. See dark-mode.