Colors
Color in Root is a small, locked set. Three seeds define the primitives. Semantic names sit on top of those primitives. Views must use the semantic names. They must not use a primitive step directly.
This site follows the system. It uses the matching aliases for the current system mode: paper canvas and primary ink in light, inverted paper and ink in dark. The ramps below still show both modes.
Seeds
Each family has one seed. The rest of the ramp comes from that seed in OKLCH. Do not pick a new hex for a product screen.
Neutral
#FBFAF9 at 100 light / 1000 dark
Blue Electric
#2A56F7 at 700, both modes
Orange Electric
#F7452A at 700, both modes
Two layers
The primitive layer is the ramp. The semantic layer is the contract with the interface. If a view needs a color, it asks for a role such as text-primary or bg-accent. It does not ask for neutral-900.
- Primitives. Neutral, Blue Electric, Orange Electric, and Neutral Alpha. These keep the ramp consistent. They are not for layout code.
- Semantics. Backgrounds, text, icons, and borders. These names stay stable when a seed changes.
There is no third layer of component tokens. If a screen needs a one-off role, add a semantic alias. Do not add a new layer.
Step roles
Every family uses the same ten steps. The role of a step does not change from family to family. The numbers are positions on the ramp, not lightness values. Step 1000 is always primary text, in both modes.
| Step | Role |
|---|---|
100 | Canvas / default background |
200 | Raised surface / hover background |
300 | Pressed surface / grouped background |
400 | Hairline divider (decorative) |
500 | Control border |
600 | Hover border |
700 | Solid brand fill |
800 | Solid fill that can hold text / hover |
900 | Secondary text and icon |
1000 | Primary text and icon |
The jump from 400 to 500 is large on purpose. 400 is a decorative hairline. 500 is an accessible control border. If you need a divider that people can see, use 500.
Families
Neutral
The paper and ink of the system. The seed is #FBFAF9. In light mode that hex sits at 100. In dark mode it sits at 1000. The paper becomes the ink.
Blue Electric
The interactive color. The seed is #2A56F7 at 700 in both modes. Use Blue only for actions that the user can touch.
Orange Electric
The moment color. The seed is #F7452A at 700 in both modes. Use Orange for emphasis, not for states.
Neutral Alpha
The same Neutral ink with a transparent channel. Use it for hover, pressed, hairlines, and overlays that must work on any surface.
Semantic aliases
These names are the public API of color in Root. Use them in views. Do not invent a new alias for a one-off screen.
Backgrounds
Paper, chrome, fills, and tinted surfaces. Dark mode inverts Neutral. Blue and Orange fills keep the 700 seed.
Text and icons
Primary, secondary, and tertiary ink. Group titles use Neutral 500. Disabled controls use Neutral 400. Inverse ink on filled controls, and the two chromatic marks.
Borders
Hairlines, control borders, and focus rings.
bg-sunken does not sink in dark mode. In light it is Neutral 300. In dark it points at the canvas. Surfaces in dark mode rise by getting lighter. There is nothing below the canvas.
text-on-accent does not invert. bg-accentkeeps the same Blue 700 in both modes, so the text on it stays paper hex #FBFAF9.
Text rank
Ink rank is primary, then secondary, then tertiary, then Neutral 500, then Neutral 400. Group titles use Neutral 500. Body uses Neutral 700. Disabled controls use Neutral 400. Active ink stays text-primary or text-secondary.
- Group titles recede with Neutral 500. They stay above disabled rows.
- Disabled controls are inactive UI. Neutral 400 is quieter than the title. Do not use Neutral 400 for live chrome.
Usage rules
- Blue marks what the user can touch and nothing else. A filled control, a link, a focus ring, and a selected state all resolve to Blue.
- Orange is for a moment. Use it on a launch mark, a rare highlight, or a product accent. Do not use Orange as success, warning, or error.
- Orange 700 is the seed. It is not a small-text color. Small text on Orange must use step 800, through
bg-expressive-strong. - Use Neutral Alpha for hover and pressed. A solid hover on a tinted card looks wrong.
- Do not mix a primitive step with a semantic alias in the same view. Pick the semantic name.
Contrast pairs
The pairs below are the intended combinations. A pair that fails its bar is marked. Orange 700 with small text fails. Use Orange 800 instead.
| Pair | Sample | Ratio | Bar | Result |
|---|---|---|---|---|
| Light / text-primary on bg-canvas | Ag | 14.68 | Small text 4.5:1 | Pass |
| Light / text-secondary on bg-canvas | Ag | 7.46 | Small text 4.5:1 | Pass |
| Light / text-tertiary on bg-canvas | Ag | 5.99 | Small text 4.5:1 | Pass |
| Light / text-disabled on bg-canvas | Ag | 4.77 | Small text 4.5:1 | Pass |
| Light / text-secondary on bg-elevated | Ag | 6.91 | Small text 4.5:1 | Pass |
| Light / border-hairline (decorative) | Ag | 1.37 | Decorative | — |
| Light / border-control | Ag | 3.12 | UI 3:1 | Pass |
| Light / border-control-hover | Ag | 3.87 | UI 3:1 | Pass |
| Light / fill-neutral with paper text | Ag | 5.99 | Small text 4.5:1 | Pass |
| Light / text-link (blue-900) | Ag | 8.84 | Small text 4.5:1 | Pass |
| Light / fill-accent + text (blue-700) | Ag | 5.57 | Small text 4.5:1 | Pass |
| Light / icon-accent (blue-700) on canvas | Ag | 5.34 | UI 3:1 | Pass |
| Light / focus-ring (blue-700) on canvas | Ag | 5.34 | UI 3:1 | Pass |
| Light / note: text-link on blue-200 | Ag | 12.14 | Small text 4.5:1 | Pass |
| Light / note: border blue-700 on blue-200 | Ag | 4.86 | UI 3:1 | Pass |
| Light / text-expressive (orange-900) | Ag | 7.78 | Small text 4.5:1 | Pass |
| Light / fill-expressive-700 + small text | Ag | 3.60 | Small text 4.5:1 | Fail |
| Light / fill-expressive-700 + large text | Ag | 3.60 | Large text 3:1 | Pass |
| Light / fill-expressive-800 + small text | Ag | 5.72 | Small text 4.5:1 | Pass |
| Light / icon-expressive (orange-700) on canvas | Ag | 3.45 | UI 3:1 | Pass |
| Light / note: border orange-700 on orange-200 | Ag | 3.15 | UI 3:1 | Pass |
| Light / note: text orange-1000 on orange-200 | Ag | 11.37 | Small text 4.5:1 | Pass |
| Dark / text-primary on bg-canvas | Ag | 19.20 | Small text 4.5:1 | Pass |
| Dark / text-secondary on bg-canvas | Ag | 8.35 | Small text 4.5:1 | Pass |
| Dark / text-tertiary on bg-canvas | Ag | 6.45 | Small text 4.5:1 | Pass |
| Dark / text-disabled on bg-canvas | Ag | 5.29 | Small text 4.5:1 | Pass |
| Dark / text-secondary on bg-elevated | Ag | 7.31 | Small text 4.5:1 | Pass |
| Dark / border-hairline (decorative) | Ag | 1.52 | Decorative | — |
| Dark / border-control | Ag | 3.34 | UI 3:1 | Pass |
| Dark / border-control-hover | Ag | 4.19 | UI 3:1 | Pass |
| Dark / fill-neutral with paper text | Ag | 2.98 | Small text 4.5:1 | Fail |
| Dark / text-link (blue-900) | Ag | 8.40 | Small text 4.5:1 | Pass |
| Dark / fill-accent + text (blue-700) | Ag | 5.57 | Small text 4.5:1 | Pass |
| Dark / icon-accent (blue-700) on canvas | Ag | 3.59 | UI 3:1 | Pass |
| Dark / focus-ring (blue-700) on canvas | Ag | 3.59 | UI 3:1 | Pass |
| Dark / note: text-link on blue-200 | Ag | 12.45 | Small text 4.5:1 | Pass |
| Dark / note: border blue-700 on blue-200 | Ag | 2.97 | UI 3:1 | Fail |
| Dark / text-expressive (orange-900) | Ag | 9.47 | Small text 4.5:1 | Pass |
| Dark / fill-expressive-700 + small text | Ag | 5.56 | Small text 4.5:1 | Pass |
| Dark / fill-expressive-700 + large text | Ag | 5.56 | Large text 3:1 | Pass |
| Dark / fill-expressive-800 + small text | Ag | 7.17 | Small text 4.5:1 | Pass |
| Dark / icon-expressive (orange-700) on canvas | Ag | 5.56 | UI 3:1 | Pass |
| Dark / note: border orange-700 on orange-200 | Ag | 4.57 | UI 3:1 | Pass |
| Dark / note: text orange-1000 on orange-200 | Ag | 12.61 | Small text 4.5:1 | Pass |
Chart family
Four hues, because four is what the paper pays for. Every fill has to clear 3:1 against Neutral 100. That caps relative luminance at 0.286. Split it four ways and neighbouring series sit 0.061 apart, which survives greyscale. Split it six ways and they do not. Four is arithmetic, not taste.
Chroma is about half the electric seeds. A data series must never be mistaken for a control. Ochre is out: below L 0.66 a yellow on warm paper is muddy. These inks are for plots and illustrations. They are not for layout.
Indigo
#4868B7 at 700
Ember
#C46150 at 700
Teal
#1B9CAB at 700
Moss
#0C5E27 at 700
| Series | Step 700 | On paper | Label on the fill | Luminance |
|---|---|---|---|---|
| Indigo | #4868B7 | 5.11:1 | Paper | 0.147 |
| Ember | #C46150 | 3.89:1 | Large text only | 0.209 |
| Teal | #1B9CAB | 3.15:1 | Ink | 0.270 |
| Moss | #0C5E27 | 7.61:1 | Paper | 0.082 |
Series order is Indigo, Ember, Teal, Moss. Two-series charts get the most separated pair first. Ember 900 can hold small paper text if the fill must carry a caption.
Indigo
Series 1. Paper labels sit on the fill. Contrast on paper is 5.11:1. Luminance is 0.147.
Ember
Series 2. No small text on Ember 700. If a label must sit on the fill, set it at 20px or larger, or move it outside. Contrast on paper is 3.89:1. Luminance is 0.209.
Teal
Series 3. Labels on the fill use ink, not paper. Contrast on paper is 3.15:1. Luminance is 0.270.
Moss
Series 4. Paper labels. Contrast on paper is 7.61:1. Luminance is 0.082.
These names are the public API of Pulse color. Views that draw a plate use them. Views that draw chrome do not. Categories start at series-1. Pictures start at illust-indigo.
Plot
What a chart asks for. Subject is still Blue. Moment is still Orange. Context is Neutral.
Series
Unordered categories only. Never a state. Never a control.
Illustration
What a picture asks for. Same inks as the series. Wash steps are Chart 400.
Open questions
- There is no error red yet. Do not use Orange for destructive actions until that decision is closed.
- There is no success green and no warning yellow. First apps should use copy and icons for that feedback.
- High Contrast is not defined. Hairline and tertiary text are the first steps that would need it.
Source of truth: foundations/tokens.json for UI color, and foundations/chart_tokens.json for Chart and Pulse inks. The scripts build_palette.py and emit.py rebuild the UI file. build_chart_palette.py rebuilds the Chart family. Do not edit a hex by hand if you plan to run the scripts.