Skip to content

refactor: migrate Chip (next) colours to beta tokens - #5225

Open
pomfrida wants to merge 4 commits into
mainfrom
refactor/chip-beta-colours
Open

refactor: migrate Chip (next) colours to beta tokens#5225
pomfrida wants to merge 4 commits into
mainfrom
refactor/chip-beta-colours

Conversation

@pomfrida

@pomfrida pomfrida commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #5223. Part of #5119 (phase 1 — colour). Second component onto the Tokens Studio beta tokens, following the Button test case (#5222). Chip has no disabled state, so it migrates completely — no legacy colour references remain.

Changes

chip.css only. All six tones consume the semantic *-interactive-* groups via --_ slots; data-color-appearance switches the tone at the chip root, with neutral as the default fallback.

Visible changes from the legacy token mapping (design-authored, not local choices):

Resolved blockers

Learnings

  • selected has no colour styling in Chip (check icon only), so the semantic selected groups (background-interactive-*-selected-*) were not exercised here — a later component or a Chip design update needs to validate those.

Verification

  • 60/60 Jest tests pass, snapshots unchanged.
  • APCA contrast (W3 0.1.9) computed from Storybook on beta.6 across all 3 variants × 6 tones × 2 colour schemes:
  • Visual pass in both modes on ToneVariantMatrix: resting chips clearly visible, hover/pressed ladder progresses correctly, high-contrast readable.

@pomfrida

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @pomfrida's task in 3m 43s —— View job


Review — Chip (next) beta token migration

Read the full chip.css, the diff against origin/chore/storybook-beta-tokens-wiring, Chip.tsx, Chip.types.ts, and the beta token bundle to verify names and values.

Verdict: clean, well-scoped migration. The --_tone-N indirection reads well, the deviations are documented up front, and it follows the EDS 2.0 conventions (--_ pseudo-private vars, override the variable not the property, data-* for variants). Everything below is minor — nothing blocking.

✅ Verified

  • All beta token names resolve. --eds-{neutral,accent,success,info,warning,danger}-{2,4,6,8,9,10,11,12}, plus --eds-text-on-accent, --eds-border-focus, and --eds-background-container-canvas-default all exist in packages/eds-tokens/src/tokens/css/variables.css.
  • Tone coverage is exact. ChipTone is neutral | accent | success | info | warning | danger. Root defaults the --_tone-* vars to the neutral scale and there's an override block for the other five — so tone="neutral" correctly falls through to the root (no dead [data-color-appearance='neutral'] block), and an unexpected value degrades gracefully to neutral. Nice.
  • Subtle ladder claim checks out. --eds-background-surface-accent-subtle-{default,hover,pressed} = accent 2/4/6 in semantic/default.css, matching the 2/4/6 fills here.
  • High-contrast contrast holds across tones (I dug into this). My first concern was that a fixed --eds-text-on-accent over a per-tone --_tone-9 fill could fail contrast on lighter hues (e.g. warning/orange). It doesn't, because the palette uses uniform lightness per step — every tone's step-9 is oklch(0.52 …) in light and oklch(0.82 …) in dark — and --eds-text-on-accent flips with the mode (light-gray-14 L≈0.90 in light, dark-north-sea-14 L≈0.33 in dark). So the text/fill lightness pairing is constant regardless of tone. Worth one manual contrast spot-check on the brightest fills (warning/success step-9), but structurally it's sound.

🟡 Questions / nits

  1. Undocumented text-colour change (default + outlined). Label colour moved from --eds-color-text-subtle--_tone-12, and for neutral --_tone-12 == --eds-text-primary (neutral-12). That's a shift from subtle to primary text. The PR carefully flags the fill deviations (step 2 vs 3, 6 vs 5) but not this one. Was the label always effectively primary-strength in prod, or is this a (probably desirable, higher-contrast) change worth noting alongside the other deviations? — chip.css:127, chip.css:143

  2. --eds-text-on-accent naming vs usage. Functionally correct as "light-on-emphasis for any tone", but the semantic name reads oddly on a danger/warning high-contrast chip. Harmless given the uniform-lightness design; just flagging in case a tone-neutral alias (e.g. an --eds-text-on-emphasis) exists or is planned in the beta set. — chip.css:160

  3. Repetition. Six ~8-line tone blocks (~48 lines) are inherent to CSS custom properties (no loops) and this matches the Button pattern, so it's the right call for consistency — noting only that adding a 7th tone or extra steps multiplies the edit surface. No action needed.

Everything else (focus ring retarget to --eds-background-container-canvas-default / --eds-border-focus, outlined border → --_tone-8, the active fills) is consistent and correct.

  • Read PR diff and full chip.css
  • Compare against Button/--_tone-N pattern and tone type
  • Verify beta token names + contrast reasoning
  • Post review
    refactor/chip-beta-colours

@pomfrida

Copy link
Copy Markdown
Collaborator Author

Re. review point 1 (text colour on default/outlined moving from --eds-color-text-subtle to --_tone-12): this is not a subtle → primary change. The legacy --eds-color-text-subtle was a dynamic token that resolved to step 12 of the active appearance scale — the published bundle defines it as var(--eds-color-{tone}-12) per data-color-appearance scope (e.g. --eds-color-text-subtle: var(--eds-color-neutral-12) under the neutral appearance). --_tone-12 preserves that mapping exactly; only the semantic name is gone, since the beta export has no equivalent dynamic token yet.

The absolute step-12 values differ marginally because the whole beta palette is regenerated, but that applies to every step and is covered by the visual parity check against prod (see PR description).

Points 2 (--eds-text-on-accent naming on danger/warning) and 3 (tone-block repetition) are known: the naming gap is tracked in #5221, and the repetition matches the Button pattern by design.

@pomfrida

Copy link
Copy Markdown
Collaborator Author

Correction to the parity claim above: the visual check against prod turned out to only cover light mode for Chip (dark was compared local-only). I've now completed the dark-mode comparison against prod with computed colours, and it is not full parity:

Matches in dark

  • Outlined: text (step 12, L 0.91 vs legacy ≈0.90) and border (step 8, L 0.76) match.
  • Default/outlined text: step 12 matches legacy.
  • High-contrast fill: step 9 (L 0.82) matches legacy exactly.

Deviations in dark

  1. Default-variant resting fill is dramatically darker. Legacy uses step 3, which in dark is L 0.47 — a clearly visible fill. The beta semantic subtle ladder (--eds-background-surface-*-subtle-default = step 2) is L 0.226 in dark, nearly identical to the canvas (step 1 = L 0.19), so default chips almost disappear into the background. The "step 2 vs 3" deviation noted in the PR description is trivial in light but large in dark.
  2. The dark scale looks like it has a generation bug: step 6 (L 0.47) is identical to step 3, and step 4 is L 0.52 — so the dark hover/pressed ladder becomes 0.226 → 0.52 → 0.47 (huge jump on hover, pressed lighter than hover). This sits in the semantic subtle ladder itself (2/4/6), not in this PR's mapping.
  3. High-contrast text: legacy is near-black in dark (rgb 3,3,3); beta --eds-text-on-accent resolves to north-sea-14 (L 0.33) — noticeably lighter on the pastel fills. Same class of deviation already flagged for light.

Items 1–3 are token-export issues rather than mapping mistakes — the PR deliberately follows the semantic subtle ladder. Flagged in #5221 for the token/design side. Whether this PR should stay on the semantic ladder (and let the fix land in the tokens) or pin steps per scheme is a design call.

Base automatically changed from chore/storybook-beta-tokens-wiring to main July 27, 2026 10:25
pomfrida added a commit that referenced this pull request Jul 28, 2026
…th the widened semantic block

The widened semantic layer and the color-scheme scope rules both apply
to [data-color-scheme] elements at equal specificity, so source order
decides the three duplicate names tracked in #5221. With the previous
alphabetical order the semantic block sorted last and won - and its
--eds-border-focus is a self-reference, computing to the
guaranteed-invalid value, which would have dropped focus outlines for
the incoming Chip (#5225) and Button (#5222) migrations inside scoped
subtrees (both use var(--eds-border-focus) for their focus ring).

The bundler now ranks color-scheme/* after every other layer, so the
scheme-specific values win on scoped elements and every resolved value
matches the pre-widening state. Verified in the browser:
--eds-border-focus resolves to the light/dark blue per subtree, and
text-disabled/border-disabled keep their scheme values.
pomfrida added a commit that referenced this pull request Jul 30, 2026
…#5239)

* fix: emit the beta semantic token layer under the color-scheme scopes

The Tokens Studio CSS export declared the semantic layer at :root while
the scale aliases it references only exist under [data-color-scheme]
scopes. Custom properties substitute where they are declared, so the
semantic tokens resolved once at the root and never re-resolved inside
[data-color-scheme] subtrees, breaking subtree colour-scheme switching
(ADR-0004/ADR-0013).

The saved EDS-CSS export configuration now sets
rootSelector: ':root, [data-color-scheme]' (was the ':root' default),
so every non-dimensional layer is also declared on any element carrying
the attribute and var() references substitute against the nearest
scope. Token content is unchanged - the diff is selector lines only.

The <html>-mirror workaround in the Storybook decorator is no longer
needed and is removed.

Closes #5226

* chore: use beta semantic tokens for the storybook wrapper canvas

The wrapper used the legacy --eds-color-bg-neutral-surface, which has no
dark declaration in the 2.x bundle, so the story canvas stayed light in
dark mode. The beta semantic tokens re-resolve per [data-color-scheme]
subtree after the rootSelector change, so the canvas now follows the
toolbar toggle. Legacy is left untouched - it is being replaced, not
maintained.

* fix: widen only the semantic layer to [data-color-scheme]

Review follow-up on #5239. Widening every non-dimensional layer via the
export's global rootSelector regressed the density cascade: the density
base (comfortable) declares the same --eds-density-* names as the
compact/relaxed variants, so re-declaring it on every
[data-color-scheme] element clobbered a [data-density] ancestor's
values for the whole subtree.

The export format has no per-layer selector, so the saved EDS-CSS
configuration is reverted to the default rootSelector (:root) and the
semantic layer - the only layer that references the scheme-scoped scale
aliases - is widened repo-side by scripts/widen-semantic-scope.mjs,
chained before generate-css-bundle.mjs in the generate:css-bundle
package script so the release workflow needs no change. The script is
idempotent and fails loudly if the export layout changes.

Also drops a comment reference to ADR-0013 (not merged yet, PR #5200)
and documents the #5221 name-collision caveat in the bundle header.

* chore: assert the semantic layer is widened before bundling

The widen step is chained in the generate:css-bundle package script,
but running the bundler directly would silently bundle an unwidened
semantic layer and regress subtree colour-scheme switching (#5226).
The bundler now fails loudly instead.

* chore: harden the semantic-widening against formatting and export drift

Review follow-up on #5239, round two.

- The widened selector is matched with whitespace-tolerant regexes and
  emitted in Prettier's canonical one-selector-per-line form, shared
  between the widen script and the bundler via semantic-scope.mjs, so
  a prettier --write no longer breaks the pipeline with a misleading
  'export layout changed' error.
- Every semantic/*.css file is widened and asserted, mirroring the
  bundler's directory glob - a file the export adds later cannot slip
  through with the narrow :root selector.
- src/tokens/* is added to .prettierignore (root and package-local):
  the directory is machine output and was never Prettier-conformant
  (the export emits double-quoted attribute selectors and no trailing
  newline, failing prettier:check on main too), and hand-formatting it
  would be reverted by the next export run. The package prettier:check
  now passes.

* chore: widen nested semantic files and document the widening in the release workflow

Review follow-up on #5239, round three.

- The widen script now globs semantic/ recursively, matching the
  bundler's assertion glob - a nested file the export adds later gets
  widened instead of deadlocking the pipeline with a remedy message
  that is a no-op.
- A selector that matches the tolerant regex but not the canonical
  form is normalised, so the committed bytes are a function of content
  only.
- The release workflow's bundle step and auto-PR body now mention the
  semantic-layer widening, so the selector diff in tokens-release PRs
  is traceable to #5226 instead of looking like an export change.

* fix: concatenate the color-scheme layer last so it wins collisions with the widened semantic block

The widened semantic layer and the color-scheme scope rules both apply
to [data-color-scheme] elements at equal specificity, so source order
decides the three duplicate names tracked in #5221. With the previous
alphabetical order the semantic block sorted last and won - and its
--eds-border-focus is a self-reference, computing to the
guaranteed-invalid value, which would have dropped focus outlines for
the incoming Chip (#5225) and Button (#5222) migrations inside scoped
subtrees (both use var(--eds-border-focus) for their focus ring).

The bundler now ranks color-scheme/* after every other layer, so the
scheme-specific values win on scoped elements and every resolved value
matches the pre-widening state. Verified in the browser:
--eds-border-focus resolves to the light/dark blue per subtree, and
text-disabled/border-disabled keep their scheme values.
@pomfrida
pomfrida force-pushed the refactor/chip-beta-colours branch from fad4bf3 to fa5aca8 Compare July 30, 2026 12:15
@vnys
vnys removed their request for review August 3, 2026 09:31
@pomfrida
pomfrida marked this pull request as draft August 5, 2026 14:25
@pomfrida

pomfrida commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Converted to draft after massive tokens change today - needs updating

@pomfrida

pomfrida commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Re-verified against local Storybook after the token realignment (4a813cc, merged main in first so the branch has the renamed tokens).

Quick background for readers without token context: every colour (neutral, accent, danger, …) comes as a row of 15 numbered shades, from lightest to darkest. The chip just picks shade numbers — shade 2 for the resting fill, 3 for hover, 4 for pressed — the same numbers the designers' own semantic tokens point to.

  • Light mode looks right. The chips are one shade lighter than prod across the board (that's what the new design prescribes), but side by side the difference is barely visible.
  • The high-contrast variant is correct in both light and dark.
  • Dark mode looks wrong, but the chip code isn't the problem — the dark shade values are. In the dark palettes the shades don't get gradually darker the way they should: shade 2 is very dark, shade 3 is suddenly much lighter, and shade 6 is identical to shade 3. The result: a resting chip is nearly invisible against the background, and hovering makes it jump in brightness. That's the palette bug tracked in Dark palette scales are non-monotonic: step 6 duplicates step 3 #5282 — once the dark shades are fixed, the chip picks them up automatically, no code change needed.
  • One question for the Dark palette scales are non-monotonic: step 6 duplicates step 3 #5282 rework: in dark mode, should resting chips be subtle (close to the background colour) or clearly grey like they are in production today? The palette fix alone doesn't decide that.

Holding this in draft until #5282 is resolved — the beta components are actively being tested, and dark mode shouldn't go out looking broken.

@pomfrida

pomfrida commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Update after today's push: Chip is now on the semantic *-interactive-* groups, following the pattern from Button (#5222) — details in the refreshed PR description.

Good news on #5283: the success/info interactive tokens have landed in Tokens Studio and are in #5284. Their values are identical to the raw steps Chip mirrors in the meantime, so once #5284 merges the swap is a pure rename.

One new thing that needs a design call: background-interactive-neutral-muted-default resolves to neutral-1 — the same value as --eds-background-canvas. That makes the resting neutral default-variant chip an invisible pill on canvas in light mode (only the text shows). Button never hits this because ghost is transparent at rest, but Chip is a filled pill. Is neutral-1 intended for filled muted surfaces, or should it sit one step up? Happy to split this into its own issue if that's easier to track.

@millus

millus commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Regarding #5283, we are not using interactive colors on the Chip text, me and Edvard dont see the use for it right now, but can change later if we were wrong. We are using static --eds-text-success (etc.) for the text colors, since the text dont change on hover/pressed, just the background color. Its updated in figma with the new success and info colors from #5284 which we merged in yesterday. Just fyi, I did write the same but a bit longer explanation on #5283 as well, so we have track of the decisions both places.

pomfrida and others added 4 commits August 13, 2026 10:10
Follows the Button pattern (#5222): background-interactive-{tone}-{muted,emphasis}-*,
text-interactive-{tone}-default, border-interactive-{tone}-emphasis-default, and
border-interactive-focus for the focus ring. Neutral maps text to --eds-text-primary
(no interactive neutral text/icon by design, #5283). success/info have no interactive
group yet (#5283) and stay on raw steps mirroring the semantic ladders.
…ractive

Use --eds-text-{tone} for label text and --eds-icon-{tone} for icons — the
two tokens differ for accent (shade 12 vs 10) and are semantically distinct
for all tones. Renames --_text-interactive to --_text and adds --_icon per
tone. High-contrast variant now overrides --_text/--_icon variables instead
of setting color directly.
@millus
millus force-pushed the refactor/chip-beta-colours branch from 8a5838a to e57aaaa Compare August 13, 2026 09:13
@millus
millus marked this pull request as ready for review August 13, 2026 09:16
@millus

millus commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto beta.6 (tokens-studio-release #5331) — both original blockers are now resolved:

#5282 (non-monotonic dark scale): The dark neutral palette is now monotonic. Steps 3–6 progress correctly in lightness (0.47 → 0.52 → 0.58 → 0.595), so hover and pressed no longer jump unexpectedly.

#5287 (neutral chip invisible on canvas): background-interactive-neutral-muted-default now resolves to neutral-3 (L 0.47 in dark), clearly distinct from canvas at neutral-1 (L 0.19). The resting chip is visible in both light and dark mode.

Also updated text and icon tokens to use the separate --eds-text-{tone} and --eds-icon-{tone} static tokens (rather than the interactive variants), following the decision in #5283 that chip text colour does not change on hover/pressed.

@millus

millus commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

@claude review

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Claude finished @millus's task in 7m 58s —— View job


Review — Chip (next) beta token migration

Read the full chip.css, Chip.tsx, Chip.types.ts, Icon.tsx, and the token sources under packages/eds-tokens/src/tokens/css/ to verify every name and value.

Verdict: correct and well-scoped. Every token resolves, the ladder values match the PR description exactly, the tone-slot indirection is clean, and the "migrates completely" claim holds. Findings below are one accessibility gap worth closing before merge and a set of nits.

✅ Verified

  • All 40+ token names resolve in semantic/default.css: background-interactive-{6 tones}-{muted,emphasis}-{default,hover,pressed}, border-interactive-{tone}-emphasis-default, text/icon-{tone}, text/icon-on-emphasis, background-canvas, border-interactive-focus.
  • Ladder values match the description: muted 3/4/5, emphasis 9/10/11, outlined border step 9, text step 12, icon step 10. No drift between the prose and the code.
  • "No legacy colour references remain" is accurate — zero --eds-color-* in chip.css. Worth noting this migration was load-bearing, not cosmetic: --eds-color-bg-fill-muted-default, --eds-color-text-subtle and --eds-color-border-focus no longer exist anywhere in packages/eds-tokens, so the pre-PR chip.css was resolving those to nothing.
  • Tone fallback is sound. No [data-color-appearance='neutral'] block; the root holds the neutral defaults, so an unexpected value degrades to neutral. Chip.tsx:52 always sets the attribute (default 'neutral').
  • Icon colouring actually works. Icon.tsx:60 renders fill={color} with default currentColor, so color: var(--_icon) at chip.css:246 resolves through currentColor. Because & .eds-icon is a descendant selector, custom icons passed in children pick up the tone too.
  • data-color-appearance has no side effects here. There are no [data-color-appearance] rules in the new token bundle at all, so the attribute is now purely a component-side selector hook — no dynamic re-scoping leaking into the chip's subtree.
  • Stylelint clean. Only the pre-existing repo-wide BEM false positive on .label (chip.css:256, untouched, every /next component has it).

🟠 Worth closing before merge

1. The contrast verification only covers resting states. Label text stays at step 12 while the fill walks 3 → 4 → 5, so contrast strictly decreases on hover and pressed in both schemes. Danger, read from colors/default.css:

resting hover pressed text (fixed)
light L 0.91 L 0.87 L 0.82 L 0.37
dark L 0.47 L 0.52 L 0.58 L 0.96

WCAG 1.4.3 applies to text in hover/active states, and the PR already reports resting danger at Lc 74.4 — below the Lc 75 target before the ladder starts eating into it. Extending the APCA table to hover + pressed (danger and warning at minimum) would either close this or produce a second token-level issue alongside #5335. Not a chip.css defect either way — the mapping follows the semantic groups as designed.

2. High-contrast text depends on source order, not specificity. .eds-chip[data-color-appearance='danger'] and .eds-chip[data-variant='high-contrast'] are both specificity (0,2,0). The --_text / --_icon override at chip.css:216-217 wins only because the TONES block (79–179) precedes it. Moving the tone blocks below the variant blocks — a plausible future tidy-up — would silently give every high-contrast chip tone-coloured text on a step-9 fill. A one-line comment stating the ordering requirement is enough. Fix this →

🔵 Nits

  1. The comment at chip.css:33 understates the icon/text split. It says the two differ "for accent: shade 12 vs shade 10", but they differ for all five non-neutral tones — and neutral is the only tone where they match, since both --eds-icon-primary and --eds-text-primary are --eds-neutral-12. Given Semantic content tokens: icon/text interactive ladders disagree, and text.accent is off-scale #5330 is tracking exactly this divergence, the comment is worth making precise. Fix this →
  2. Two dead transitions. After this migration nothing but background-color changes on hover/pressed — text and border are static per tone — so color 150ms and outline-color 150ms (chip.css:72-75) are no-ops.
  3. --_border-interactive is off-pattern. e57aaaa dropped -interactive from --_text-interactive; the border slot kept it while --_bg-* / --_text / --_icon didn't. --_border would be consistent.
  4. disabled is still in the API with zero styling. ChipProps extends ButtonHTMLAttributes without omitting it (Chip.types.ts:79), so <Chip disabled> type-checks and renders a fully saturated chip that still hover-highlights (:hover matches disabled buttons) with cursor: pointer. The PR's "Chip has no disabled state" is true of the design but not of the API. Omit<…, 'disabled'> would make it explicit — out of scope for a CSS-only PR, but a good follow-up.
  5. Focus-ring width has no fallback (chip.css:235). I could not find --eds-sizing-stroke-thin defined anywhere in the tracked sources — the new token source only ships colour/density/font/primitives, and packages/eds-tokens/build/ is gitignored and read-blocked in this environment, so I can't confirm it survives the migration. If it ever resolves to nothing the whole box-shadow is invalid at computed-value time and there is no focus indicator (WCAG 2.4.7). The neighbouring --eds-border-width-default, 1px does carry a fallback. Pre-existing and repo-wide (Select, Link, Checkbox, Accordion, Badge) — just worth a , 1px.
  6. Focus-ring gap assumes canvas (--eds-background-canvas, chip.css:235). Chips commonly sit in cards and toolbars, where the 1px gap ring will be the wrong colour. Pure rename in this diff, so pre-existing.

Not verified

I could not run the Jest suite: pnpm test runs tsc first, and that fails on missing @equinor/eds-tokens-build artefacts (pnpm --filter @equinor/eds-tokens run build fails in this environment). The change is CSS-only and snapshots don't capture computed colour, so the suite wouldn't catch colour regressions regardless — the APCA table is the real verification here, which is why point 1 matters.

  • Read PR diff and full chip.css
  • Verify every --eds-* token name and value against the token sources
  • Check Chip.tsx / Chip.types.ts / Icon.tsx interaction with the new vars
  • Run stylelint (clean); Jest blocked by unrelated build failure
  • Post review
    refactor/chip-beta-colours

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Chip] Migrate to new beta colour tokens

2 participants