Parent: #4740
Problem
The semantic border.<tone>.<strength> ladder was incomplete and internally inconsistent. Only accent and neutral had all three strengths; the four status tones had subtle alone:
| tone |
subtle |
medium |
strong |
| accent |
accent.4 |
accent.7 |
accent.9 |
| neutral |
neutral.4 |
neutral.4 ⚠️ |
neutral.6 |
| danger / info / success / warning |
*.4 |
— |
— |
Two separate defects:
- Eight tokens simply missing — no
medium/strong for danger, info, success, warning. There was no way to express a mid- or high-emphasis status border.
border.neutral.subtle and border.neutral.medium resolved to the identical colour ({neutral.4}), so of neutral's three named strengths only two were visually distinct.
Root cause
Not a design decision — a migration artifact of the rename in #5280.
Before that PR the semantic layer had no status borders at all, only neutral chrome plus one accent token:
--eds-border-subtle: var(--eds-neutral-2)
--eds-border-default: var(--eds-neutral-4)
--eds-border-emphasized: var(--eds-neutral-6)
--eds-border-hover: var(--eds-neutral-7)
--eds-border-selected: var(--eds-accent-13)
#5280 restructured these into border.<tone>.<strength>. neutral's subtle/medium/strong is a literal 1:1 rename of border-subtle/-default/-emphasized — three levels existed, so three got names. The four status tones were new in that PR and only got the single level messages/banners needed at the time. Nothing was dropped; the rest was never created.
The duplicate then appeared in #5284, which bumped border.neutral.subtle from {neutral.2} to {neutral.4} — sensible on its own, since ladder steps 1–2 are background planes and read as invisible borders, but medium was already {neutral.4} and wasn't adjusted to compensate.
Resolution
Adopt subtle = step 4 · medium = step 7 · strong = step 9 for all six tones.
That is accent's existing mapping, and the only one that sits on the band already used everywhere else in the semantic set — 4 = interactive.muted.default, 7 = interactive.muted.pressed, 9 = interactive.emphasis.default. Every other slot in semantic uses the same ladder step across all six tones, so border was the lone exception. Neutral's 4/4/6 matched nothing.
Lightness separation from input/scale:
| step |
light L |
dark L |
4 subtle |
0.87 |
0.52 |
7 medium |
0.72 |
0.61 |
9 strong |
0.52 |
0.82 |
Applied in Tokens Studio on branch border-tone-ladder — 8 creates plus 2 value updates, each new token carrying scopes: ["STROKE_COLOR"], hiddenFromPublishing: false and codeSyntax.WEB. Awaiting human merge in the Studio UI.
New CSS custom properties once released:
--eds-border-danger-medium --eds-border-danger-strong
--eds-border-info-medium --eds-border-info-strong
--eds-border-success-medium --eds-border-success-strong
--eds-border-warning-medium --eds-border-warning-strong
⚠️ Visually breaking
border.neutral.medium and border.neutral.strong change value. In light mode medium goes L 0.87 → 0.72 and strong L 0.77 → 0.52, so any card, divider or input border bound to those two tokens gets noticeably darker. Worth a visual pass on components before this ships.
Verification
Verified by direct GET on the branch (tsctl diff is blind to extensions-only writes):
- 1270 → 1278 tokens; exactly 8 added, 0 removed
- exactly 2 pre-existing tokens changed, with no drift in any other token's value, scopes,
hiddenFromPublishing or codeSyntax
- all 57
border.* tokens carry STROKE_COLOR
- no CSS-variable name collisions on main or branch
- resolver: 1016 keys, 0 unresolved, 0 errors; light resolves to L 0.87 / 0.72 / 0.52 exactly as
input/scale predicts
Dark could not be verified through /resolved_tokens — passing the dark color-scheme theme option returns light values even for a control token that must differ (background.canvas gives oklch(0.98 0 0) under both). No new reference risk regardless: {tone.7} and {tone.9} are already resolved in both schemes by the existing border.interactive.*.muted.pressed and .emphasis.default tokens for every tone.
Follow-ups
Parent: #4740
Problem
The semantic
border.<tone>.<strength>ladder was incomplete and internally inconsistent. Onlyaccentandneutralhad all three strengths; the four status tones hadsubtlealone:accent.4accent.7accent.9neutral.4neutral.4neutral.6*.4Two separate defects:
medium/strongfordanger,info,success,warning. There was no way to express a mid- or high-emphasis status border.border.neutral.subtleandborder.neutral.mediumresolved to the identical colour ({neutral.4}), so of neutral's three named strengths only two were visually distinct.Root cause
Not a design decision — a migration artifact of the rename in #5280.
Before that PR the semantic layer had no status borders at all, only neutral chrome plus one accent token:
#5280 restructured these into
border.<tone>.<strength>.neutral'ssubtle/medium/strongis a literal 1:1 rename ofborder-subtle/-default/-emphasized— three levels existed, so three got names. The four status tones were new in that PR and only got the single level messages/banners needed at the time. Nothing was dropped; the rest was never created.The duplicate then appeared in #5284, which bumped
border.neutral.subtlefrom{neutral.2}to{neutral.4}— sensible on its own, since ladder steps 1–2 are background planes and read as invisible borders, butmediumwas already{neutral.4}and wasn't adjusted to compensate.Resolution
Adopt
subtle= step 4 ·medium= step 7 ·strong= step 9 for all six tones.That is accent's existing mapping, and the only one that sits on the band already used everywhere else in the semantic set —
4=interactive.muted.default,7=interactive.muted.pressed,9=interactive.emphasis.default. Every other slot insemanticuses the same ladder step across all six tones, so border was the lone exception. Neutral's4/4/6matched nothing.Lightness separation from
input/scale:subtlemediumstrongApplied in Tokens Studio on branch
border-tone-ladder— 8 creates plus 2 value updates, each new token carryingscopes: ["STROKE_COLOR"],hiddenFromPublishing: falseandcodeSyntax.WEB. Awaiting human merge in the Studio UI.New CSS custom properties once released:
border.neutral.mediumandborder.neutral.strongchange value. In light modemediumgoes L 0.87 → 0.72 andstrongL 0.77 → 0.52, so any card, divider or input border bound to those two tokens gets noticeably darker. Worth a visual pass on components before this ships.Verification
Verified by direct GET on the branch (
tsctl diffis blind to extensions-only writes):hiddenFromPublishingorcodeSyntaxborder.*tokens carrySTROKE_COLORinput/scalepredictsDark could not be verified through
/resolved_tokens— passing the darkcolor-schemetheme option returns light values even for a control token that must differ (background.canvasgivesoklch(0.98 0 0)under both). No new reference risk regardless:{tone.7}and{tone.9}are already resolved in both schemes by the existingborder.interactive.*.muted.pressedand.emphasis.defaulttokens for every tone.Follow-ups
border-tone-ladderin the Studio UI, then releaseborder.neutral.medium/.strongborder.interactive.neutral.mutedis4/7/8where the other five tones are4/5/7— neutral's other outlier from the same rename, not addressed here4/7/9rule currently exists only implicitly in the values