Skip to content

feat: align semantic color tokens with the Figma export - #11

Open
anmolsinghbhatia wants to merge 4 commits into
mainfrom
feat/align-semantic-color-tokens
Open

feat: align semantic color tokens with the Figma export#11
anmolsinghbhatia wants to merge 4 commits into
mainfrom
feat/align-semantic-color-tokens

Conversation

@anmolsinghbhatia

Copy link
Copy Markdown
Contributor

Description

Audited all 203 tokens in each of the four theme files from the Figma "Colors - Semantics" collection against variables.css, resolving both sides to sRGB and comparing values and primitive references. 29 tokens had drifted; this closes 26.

Priority is the substantive fix. --priority-* was declared once at :root, so dark and dark-contrast rendered light-mode priority colors — and the light values were wrong at every step too. It now resolves per theme and gains a sixth step, --priority-unlit, exposed as text-priority-unlit and border-priority-unlit.

Also corrected: the --neutral-800 (light) and --neutral-white (dark) primitives, which pulled text-disabled, text-on-color, text-icon-on-color, border-inverse and illustration-stroke-secondary off-spec; the light icon ladder, where text-icon-tertiary, text-icon-placeholder and text-icon-disabled each sat one rung too light, with the matching light-contrast overrides added; label steps for orange, pink, grey, indigo and purple; and the absent contrast overrides for text-warning-primary and label-purple-bg-strong.

border-info-strong in dark-contrast drew from --blue-400, which sits at the dark end of the inverted dark ramp — a navy edge on a dark canvas. It now matches the other intents.

--scrollbar-thumb-active drops from 40% to 30% alpha per the export, making the press state identical to hover.

Docs and Storybook now cover the whole namespace. The color page was hand-curated — ROLE_SPECS listed 41 of 153 bg/text/border tokens, and the Status role covered fills only — so border-danger-strong shipped, resolved differently in all four themes, and appeared nowhere, while the prose referenced ring-danger-strong. The page now lists every token, generated by enumerating @theme inline rather than by hand, and resolves all four themes instead of light and dark. A new Foundations/Color story covers the same ground live, guarded by a test that fails if the CSS gains or loses a token the list does not carry.

Three things need fixing in Figma

None of these block this PR, but the workarounds should go away once the source is corrected.

  1. text/priority/* does not switch in dark modes. It still resolves to priority/light/* in Dark and Dark contrast while icon/priority/* correctly switches to priority/dark/*, so the same semantic colour has two values inside one theme. This follows icon/priority/* — a priority glyph and its label have to agree — leaving five tokens deliberately diverging from the export.
  2. Three spec values are below WCAG AA where the previous ones were above it, and are carried anyway because they are the published spec: label-orange-text on its own background in light is 3.00:1 (was 5.57:1), label-pink-text is 4.04:1 (was 5.50:1, and it already failed in dark), and --neutral-800 is 4.23:1 on white (was 4.54:1). color-contrast is disabled on the two Badge Variants stories for the label pair, with the reason recorded inline.
  3. scrollbar/thumb-active equals thumb-hover at 30% in the export, so the press state is not distinguishable. Followed as published; worth confirming it is intended.

Incidental fix

PreviewCardEyebrowLabel moves from text-disabled to text-tertiary. It painted content with the one token exempt from contrast requirements, and only passed a11y because --neutral-800 sat 0.04 above the AA threshold before this correction.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Documentation update

Screenshots and Media (if applicable)

Test Scenarios

  • Foundations/ColorPriority, switching all four themes: the ramp changes between light and dark, and unlit sits clearly below none without dropping out of the surface
  • Foundations/ColorStatusBorders in dark-contrast: the info edge is visible and consistent with the other intents
  • Foundations/ColorAllTokens: every family renders, no blank swatches, ring and outline draw indicators rather than squares
  • Docs → Foundations → Color → "Every token": 227 tokens across eight families plus 57 label tokens, four swatches each
  • Remove a token from @theme inline and re-run pnpm test: color-token-list.test.ts fails with the missing name
  • Any Badge with variant="orange" or variant="pink" in light: label text is the published Figma value, and reads as low-contrast by design pending the source fix
  • PreviewCard with an eyebrow label: the identifier text is one step muted from the description and no longer uses text-disabled
  • pnpm check and pnpm test green (1476 tests)

References

No Plane work item — raised during a token audit of the design team's Figma export. Items 1–3 above need raising with design.

🤖 Generated with Claude Code

Audited all 203 tokens in each of the four theme files against variables.css,
resolving both sides to sRGB and comparing values and primitive references.
29 tokens had drifted; this closes 26 of them.

Priority is the substantive change. --priority-* was declared once at :root, so
dark and dark-contrast rendered light-mode priority colors, and the light values
were wrong at every step. It now resolves per theme and gains a sixth step,
--priority-unlit, exposed as text-priority-unlit and border-priority-unlit.

Figma's text/priority/* still points at the light ramp in both dark themes while
icon/priority/* switches correctly, so the two disagree inside one theme. This
follows icon/priority/* — a priority glyph and its label have to match — which
leaves five tokens deliberately diverging from the export until Figma is fixed.

Also corrected: the --neutral-800 (light) and --neutral-white (dark) primitives,
which pulled text-disabled, text-on-color, text-icon-on-color, border-inverse
and illustration-stroke-secondary off-spec; the light icon ladder, where
text-icon-tertiary, text-icon-placeholder and text-icon-disabled each sat one
rung too light, with the matching light-contrast overrides added; label steps
for orange, pink, grey, indigo and purple; and the absent contrast overrides for
text-warning-primary and label-purple-bg-strong.

border-info-strong in dark-contrast drew from --blue-400, which sits at the dark
end of the inverted dark ramp — a navy edge on a dark canvas. It now matches the
other intents.

--scrollbar-thumb-active drops from 40% to 30% alpha per the export, making the
press state identical to hover, and the ladder comment no longer claims a fourth
step.

Three corrected values are below WCAG AA where the previous ones were above it,
and are carried anyway because they are the published spec: label-orange-text on
its own background in light is 3.00:1 (was 5.57:1), label-pink-text is 4.04:1
(was 5.50:1, and it already failed in dark), and --neutral-800 is 4.23:1 on white
(was 4.54:1). The color-contrast rule is disabled on the two Badge Variants
stories for the label pair, with the reason recorded inline. These need raising
at source.

PreviewCardEyebrowLabel moves from text-disabled to text-tertiary. It painted
content with the one token exempt from contrast requirements, and only passed
because --neutral-800 sat 0.04 above the AA threshold before this correction.
There was no colour story, so the tokens the audit corrected could only be
reviewed by reading resolved values on the docs site. Five specimens cover the
ladders where the gap between steps is the thing under review — priority, the
text and icon ladders, status borders, labels, and the scrollbar thumb — plus an
AllTokens story rendering the full namespace.

Each family is painted the way it is consumed: backgrounds fill, text colours a
glyph, borders draw an edge, ring and outline draw their indicator, and fill-*
and stroke-* paint real SVG geometry. A flat grid of squares would show
ring-danger-strong as a square rather than a ring.

Utility names live in color-token-list.ts as literals rather than being
assembled at runtime, because @theme inline inlines the semantic properties into
utilities instead of emitting them: var(--text-priority-urgent) paints nothing,
and Tailwind only generates a utility it has seen as a literal in the source.
color-token-list.test.ts reads the @theme inline block and fails if the CSS
gains or loses a token the list does not carry, so "every token" is checked
rather than asserted in a comment.

The story disables color-contrast. A specimen sheet is not an interface, and
several of these tokens are meant to fail as running text — text-disabled is
exempt by definition, text-priority-unlit is a dimmed indicator segment, and the
label hues carry the Figma values verbatim.

Themes come from the toolbar rather than side-by-side panels: the tokens are
declared on :is(:root, :host), so a nested data-theme wrapper never re-resolves
them and a four-up comparison in one canvas is not possible.
The colour page was hand-curated: ROLE_SPECS listed 41 of the 153 bg/text/border
tokens, and the Status role covered fills only. So border-danger-strong shipped,
resolved differently in all four themes, and appeared nowhere — while the prose
two sections down referenced ring-danger-strong.

Adds an "Every token" section built by enumerating the @theme inline block
rather than listing names by hand: 227 tokens across eight families, alongside
the 57 label tokens the existing grid already covered. It throws at build time
if a token matches no family prefix, so a new utility namespace cannot ship
undocumented.

color-tokens.ts resolved light and dark only, which meant the contrast themes —
the whole point of the audit — were invisible on the site. It now resolves all
four by layering each variant block over its base the way the cascade does, and
ColorRole, LabelGrid and the new ColorIndex all show four swatches. Values
collapse to a single entry when a token resolves the same everywhere, so the
common case does not read as four unrelated colours.

literal() now accepts `transparent`: the scrollbar thumb is genuinely nothing at
rest, and it is a real token the index has to show. The curated roles never hit
that path.

LABEL_TOKEN_COUNT is exported rather than computed in the page, because MDX
inline expressions are markdown first — a `*` in {a.length * b.length} is
escaped to `\*` by the formatter, which turns a working expression into a
render-time syntax error that typechecking does not catch.

Also notes the two label hues that do not clear AA on their own background.
@anmolsinghbhatia anmolsinghbhatia self-assigned this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants