Color
Semantic color tokens, interaction states, status colors, and the primitive palettes they reference.
Color tokens define the visual identity across all BDS themes. The system separates primitive color scales (raw hex values) from semantic color roles (purpose-bound aliases). Components reference the semantic roles only.
Never use a token outside its semantic category. --text-* is for text. --background-* is for backgrounds. --border-* is for borders. No --text-primary on a background-color. The cascade is a contract — bypassing it leaks brand drift everywhere it isn't caught.
Click any swatch to copy var(--token-name) to your clipboard.
Page and surface
Background
Text
Border
Interaction states
Hover, press, and disabled. These replace filter: brightness() and opacity hacks — the proper tokens compose with the rest of the cascade. These are the swatches; for when to reach for a solid state-pair token versus a composable overlay, see Interaction States.
Status
Canonical Figma tokens for positive, negative, and warning states. Background variants are saturated; surface variants are subtle tints.
The info signal — blue, and one of the five valence values. It came from Figma under the status- segment until #1959 freed the name: the gray family that used to hold --*-info is now --*-neutral, so info means blue and only blue. The --*-status-info* spellings still resolve as deprecated aliases for one minor (#1958 deletes them). There is no --border-info — the gray one retired with the rename and blue has no border counterpart.
The neutral intent family — gray, Figma-sourced across all four purposes since #1959. Note the purpose split the rename made honest: --background-neutral is the saturated charcoal, --surface-neutral the subtle tint. A gray Badge or Tag chip wants --surface-neutral.
Hues with no valence — purple and orange are hue names, not valence values, so ADR-033 § 1's five words have no slot for them. They belong to the hue-source scope documented under Accent below and are Figma-sourced there, so #1958 retired the --background-status-{purple,orange} spellings into it rather than amending the ADR.
Accent
The scope-stable accent — one brand highlight that stays fixed regardless of [data-audience] scope (brik fills it from tan; each Brand Kit fills its own). Use it for audience-independent affordances such as an active carousel dot or an eyebrow rule.
It is distinct from two neighbours it is easy to confuse:
--*-brand-primaryre-tints per audience scope (Vale: land → gold, healthcare → olive). The accent must not shift, so it is its own role — not a brand-tier token.- The per-color status accents (
--background-accent-blue,--text-accent-green, …) encode state/category, not the brand highlight.
Brand Kits fill --*-accent per client (brik → tan, Vale Partners → gold). Because the audience scopes re-bind only the brand-primary tier, the accent stays constant across every [data-audience] — that scope-stability is the whole point of the role.
The per-hue accent set
Separate from the scope-stable accent above, accent-{hue} names a category color — six hues for tagging things that have no valence. A calendar's event categories, a chart series, a tag palette.
The set is mirrored 1:1 from the ❖ Brik Brand Kit Figma library, which is the source of truth for it. Five hues fill from the system palette — that is what keeps accent-purple the same purple whichever client theme is loaded — and the purpose split is the one the valence families use:
| Purpose | Fills from | Use for |
|---|---|---|
--text-accent-{hue} | --color-system-{hue} — the vivid step | Colored label or icon on a neutral surface |
--background-accent-{hue} | --color-system-{hue} — the vivid step | A saturated fill: a dot, a solid chip, a chart mark |
--border-accent-{hue} | --color-system-{hue} — the vivid step | A hairline or left-rule keyed to the category |
--surface-accent-{hue} | --color-system-{hue}-light — the pale tint | A wash behind content: a subtle chip, a callout, a row highlight |
--page-accent-{hue} | --color-system-{hue}-light — the pale tint | A full-page or section-wide wash |
The grid is deliberately not square, because canon isn't. Two gaps are mirrored from Figma rather than filled in:
--page-accent-{hue}exists foryellow,blue,orangeandredonly — there is no page-level green or purple wash.redis the exception to the system-palette rule. All five of its purposes alias the brand poppy (#e35335), not--color-system-red. So--surface-accent-redis a saturated fill, not a pale tint like the other five — it is gated AA rather than AAA in the matrix, and small body copy does not belong on it.Avatar's red initials fill is the shipped consumer.
Two consequences worth knowing before you reach for one:
- Put dark ink on a
surface-accent-{hue}, not--text-primary. The tints hold the same pale value in both themes while--text-primaryflips to near-white in dark, which lands at 1.2:1.--text-on-color-lightis the theme-stable pairing and clears AAA on all five tints (AA on the red fill) — the sixaccentrows in the matrix gate it. --text-accent-{hue}is not a foreground for--surface-accent-{hue}. Same-hue vivid-on-pale only clears AA forblue; the other four fall below the 3:1 floor (#2080).
--surface-accent-{hue} shipped in #2065 pointing at the Brand Kit mid-tone ramp (--color-{hue}-light, itself a deprecated alias for the -500 step). #2066 re-pointed it onto the system tint, then reconciled the whole family against the Figma Brand Kit — adding --border-accent-{hue}, --page-accent-{hue} and --surface-accent-red, and moving accent-red onto the brand poppy. Dark ink on the purple tint went from 5.68:1 to 12.87:1 in the process.
Presence
Online/offline indicator tokens used by Avatar and Dot components.
Color modifiers
Within the color role/modifier vocabulary, four concepts share light/dark wording but answer different questions. Pick the wrong one and it compiles — silently wrong.
on-color
The legible foreground for content resting on a colored, branded, or dark surface. Grayscale, brand-agnostic, consumed by component variants (Button on-color, the Tab-bar onColor mode).
--bds-text-on-color-light /* dark FG — for a light colored fill */
--bds-text-on-color-dark /* white FG — for a dark colored fill */on-color is the whole role; light/dark names the fill tone it targets. It never carries a hue.
-on-light and -on-dark
For a token whose hue is already named (a service line, a brand scope), pins the value for a known, fixed backdrop tone.
--bds-background-service-brand-on-light /* brand fill, on a light surface */
--bds-text-service-brand-on-dark /* brand text, on a dark surface */The -color- infix is dropped because the scope (service-brand) already implies color. The leading on- marks context — distinct from tone.
Two on forms, on purpose. on-color-* is a complete neutral role; -on-light/-on-dark is a context modifier on a colored scope. Same word, different slot in the formula — not an inconsistency, and not a rename candidate.
-inverse
The hard reverse across light/dark mode (lightest ↔ darkest), mirroring the reverse of a -primary token. A theme-level concept — --background-inverse, --text-inverse. A service line normally adapts to a known backdrop through its -on-light/-on-dark context tokens, not a per-line inverse — with one carve-out: --surface-service-{line}-inverse (surface purpose only) flips white in light → {hue}-darkest in dark, for service-identified cards that must read as neutral chrome in light mode but carry the line's deep tint in dark. See ADR-020. No per-line inverse exists on background/text/border.
-light and -dark
The pastel or deep shade of a surface (--surface-service-brand-light = pastel). Tone shifts the shade; context targets the backdrop. Tone is why scoped context had to be spelled -on-light/-on-dark — -light/-dark was already taken.
| You want… | Token |
|---|---|
| Legible text/icon on any colored or dark fill | --{purpose}-on-color-{light|dark} |
| A service hue tuned for a known backdrop | --{purpose}-service-{line}-on-{light|dark} |
| An element that hard-flips with the theme | --{purpose}-inverse |
| A pastel/deep variant of a service surface | --surface-service-{line}-{light|dark} |
| A service card surface: neutral white in light, deep tint in dark | --surface-service-{line}-inverse |
The 11-step scale
Every family is an 11-step numeric ramp — 50, 100, 200, … 900, 950, lightest to darkest. 50 and 950 bracket the ends; the middle nine carry the working values. The scale is generated, not hand-tuned per family: scripts/generate-color-ramps.mjs pins the six historical swatches as anchors and interpolates the other five stops as OKLCh midpoints, writing design-tokens/color-ramps.generated.json (#1737). Shipped in v0.152.0.
Reach for a stop by its number. --color-{family}-{stop} is the Primitive-tier name; components still consume Semantic tokens only.
Deprecated 6-step aliases
The old ladder (lightest → lighter → light → dark → darker → darkest) still resolves — #1739 aliased each name onto its stop so nothing broke on upgrade. The aliases are deprecated and land at these positions in every family:
| Deprecated alias | Resolves to | Anchor? |
|---|---|---|
--color-{family}-lightest | 100 | anchor |
--color-{family}-lighter | 300 | anchor |
--color-{family}-light | 500 | anchor |
--color-{family}-dark | 700 | anchor |
--color-{family}-darker | 800 | anchor |
--color-{family}-darkest | 950 | anchor |
Note the ladder was never evenly spaced: dark and darker sit one stop apart (700/800) while darker to darkest skips 900. Prose that said "one step deeper" under the 6-step names does not translate to one numeric stop — count stops, don't translate names.
Don't write a deprecated alias into new code. npm run lint-mdx-tokens fails the build on one in a docs code block or table, and lint-tokens Rule 12 covers component CSS. Consumer migration is tracked in #1740; the aliases are removed only once it closes.
The two new stops that matter
Five stops per family are new. Two of them close specific, filed gaps:
| Stop | Value | Why it exists |
|---|---|---|
--bds-color-poppy-600 | #c94428 | 4.84:1 on white — clears AA (4.5:1). The near-Poppy step ADR-015/ADR-016 said did not exist. Not the button fill: ADR-015 (2026-10-22) keeps buttons on vibrant Poppy |
--bds-color-grayscale-400 | #b0b0b0 | Fills the #828282 → #e0e0e0 gap from #1726. It is not #bdbdbd — no even OKLCh ramp lands there, so consumers that hand-pinned #bdbdbd are migrating to this value, not matching it |
Colour families by purpose
Four colour groups sit beside the numeric ramps. Each has one purpose, declared in tokens/naming-grammar.json ($purposes). Pick by purpose, not by how the hue looks.
| Family | Purpose | Rule for agents |
|---|---|---|
system | Messaging. Error, success, warning, neutral and info colours for Alert, Badge and Toast, plus link blue and accent hues. | Never read --bds-color-system-* in a component. Read the Semantic that aliases it, such as --bds-text-error. lint-tokens rule 18 fails the build otherwise. |
social | Third-party brand marks. YouTube, X/Twitter, Facebook, Instagram, LinkedIn, Yelp, Google, Apple, Bing and TikTok, for SocialIcon. | These are the platforms' colours, not Brik's. Never use one as a UI accent. |
annotation | Annotation tooling. Mark-up colours for design and doc annotation. | Never in product UI. |
grayscale-black, grayscale-white | Brand-guide-invariant constants. Foundation black and white that every brand guide carries. | A brand kit never rebinds them. |
transparent and lightbox are utility constants that keep their system names as grammar namedExceptions. They are not messaging colours. Full rules: Token Anatomy.
Primitives
Raw color scales from the Figma Brand Kit, rendered from color-ramps.generated.json. Components reference these via semantic tokens — never directly. The primitives change rarely; the semantic mappings change per theme.
Grayscale
Poppy (brand primary)
Tan
Orange
Yellow
Green
Blue
Purple
Pink
Related
- Color Pairings — which text token is WCAG-safe on which surface (brand + service tiers, both themes), the contrast matrix, and the CI gate
- Interaction States — the overlay-vs-solid model behind the hover/press/disabled swatches above
- Token Anatomy — the four-tier model and the naming formula these roles are composed from (the color-modifier vocabulary —
on-color,-on-light/-on-dark,-inverse, tone — is documented above under Color modifiers) - Color primitive tier — the closed
tiervocabulary, tonal-scale emission rules, and the deprecated suffix-alias migration - Client Themes → Per-audience scope binding — multi-brand scope-based token re-binding for sites like Vale Partners and brikdesigns.com
- Cascade rules — the three-layer architecture every consumer follows
- Atmospheres — the mood overlay layer that sits on top of theme tokens
- Storybook → Color contrast compliance
Token Anatomy
The seven disambiguated concepts that describe every BDS token — Anatomy, ID, Tier, Library, Layer, Mode, Tenet — and the four-tier abstraction stack every token belongs to.
Color Pairings
The accessible foreground/background pairing system — which text token is WCAG-safe on which surface, for brand and service-tier colors, in both themes, enforced by a CI gate and portable to client themes.