import { CopyBlock } from '~/chrome/ui/copy'; /* * `Paragraph` and `SubHeading` are aliased back to the one-letter names this * document reads best with — `
` appears 55 times in a page that is prose. * `ProseTable` is not aliased on purpose: `Table` is the name `chrome/ui/tables.tsx` * exports, and shortening it here is how a future import of that one becomes a * collision. */ import { Finding, Hex, Paragraph as P, ProseTable, SubHeading as H3 } from '~/chrome/ui/prose'; import { useMessages } from '../i18n'; import { Link } from '../router'; /** * The narrative half of Figma parity: what was found, the evidence, and which * side has to move. * * Ported from `docs/FigmaParity.mdx` when Storybook became optional. It is * deliberately separate from `parity.tsx`, which is generated from * `figma-spec.json` — that page tells you *where* the two disagree, this one * tells you *why* and what to do about it. A finding only lands here once * somebody has resolved it against the variable table. * * Kept in English, unlike the rest of the site: it is an audit record aimed at * whoever is holding the Figma file open, and it is mostly node ids, hex values * and variable paths — none of which translate. The tab says so. */ export function ParityFindings() { const m = useMessages(); return (
File: CBAR - Design System (Copy) ·{' '}
Date: 2026-08-02, §7 added 2026-08-06 ·{' '}
Method: the local bridge plugin (
figma_variables, figma_node, figma_css), read live
— no REST, no cached export.
Discrepancies found between CBAR’s Figma file and this kit, with the evidence for each and which side has to change. They are written down rather than fixed in passing because every one of them is a decision for the design file, not for the code.
{m.parity.findingsEnglishOnly}
Button{' '}
(node 2446:6959, page “Button”) and avatar (node{' '}
2446:757, page “Avatar”).{' '}
Who is wrong: Figma. This kit is correct
and must not be changed.
>
}
>
Every colorPalette=primary variant of Button paints with the{' '}
secondary (turquoise) role, and every colorPalette=secondary variant
paints with the primary (navy) role. It holds across all five treatments, not
just solid:
surface/colored/secondary,
surface/colored/primary,
],
[
solid,
secondary,
surface/colored/primary,
surface/colored/secondary,
],
[
subtle,
primary,
surface/subtle/secondary,
surface/subtle/primary,
],
[
subtle,
secondary,
surface/subtle/primary,
surface/subtle/secondary,
],
[
outline,
primary,
border/colored/secondary,
border/colored/primary,
],
[
outline,
secondary,
border/colored/primary,
border/colored/secondary,
],
[
surface,
primary,
surface/subtle/secondary + border/colored/secondary,
…/primary
,
],
[
surface,
secondary,
surface/subtle/primary + border/colored/primary,
…/secondary
,
],
]}
/>
ghost draws neither a fill nor a border for either palette, so it is
unaffected.
Scope: 160 of the set’s 560 variants — 2 palettes × 5 treatments × 4 states × 4 sizes.
Found while building{' '} the avatar Figma-layout tab , which reads each band’s fill rather than trusting its name. All three of avatar’s treatments are crossed the same way Button’s five are:
surface/colored/secondary,
surface/colored/primary,
],
[
solid,
secondary,
surface/colored/primary,
surface/colored/secondary,
],
[
subtle,
primary,
surface/subtle/secondary,
surface/subtle/primary,
],
[
subtle,
secondary,
brand/primary/50,
surface/subtle/secondary,
],
[
outline,
primary,
border/subtle/secondary,
border/subtle/primary,
],
[
outline,
secondary,
border/subtle/primary,
border/subtle/secondary,
],
]}
/>
One row is wrong twice over. Avatar’s subtle /{' '}
secondary cell binds brand/primary/50 — a raw ramp step rather
than a semantic role, and one step lighter than the …/100 the other
treatments read. Rebinding it to surface/subtle/secondary fixes both at
once. Note also that avatar’s outline takes its stroke from{' '}
border/subtle/* where Button takes it from border/colored/*;
that difference is the set’s own choice and is left alone here.
Scope: 72 of the set’s 150 variants —
2 palettes × 3 treatments × 2 shapes × 6 sizes. The six{' '}
avatar-type=avatar variants are drawn only on third, so they
are unaffected.
Everything else about avatar matches: the box ladder (24 / 32 / 36 / 40 / 48 / 64px), the type ladder (10 / 12 / 14 / 16 / 20 / 24px) and the flat 8px square corner are identical to what the kit draws, measured rung by rung through the bridge.
Three independent places in the same file disagree with Button:
figma_variables, the layer the REST API refuses):
{`brand/primary/500 = #004976 (navy)
brand/secondary/500 = #4BC7B5 (turquoise)
surface/colored/primary -> brand/primary/500
surface/colored/secondary -> brand/secondary/500
border/colored/primary -> brand/primary/300
border/colored/secondary -> brand/secondary/200`}
2399:522),
Button’s closest sibling — variant=solid, state=default, size=md:
primary 2264:23) —
track stroke at size=md: primary
And this kit: src/styles/tokens.css names CBAR’s Primary ramp{' '}
--ui-color-brand-* (.palette-primary) and its Secondary ramp --ui-color-secondary-* (
A — the variant names are wrong (recommended).{' '}
Swap the colorPalette value on all 232 crossed variants — 160 on Button, 72
on avatar: what is called secondary today becomes primary, and
vice versa. Bindings are left alone.
The argument for A is each set’s own default. Button’s is{' '}
colorPalette=secondary and renders navy, so CBAR’s default button
already is the brand primary colour and only the label on it is wrong.
Avatar’s is colorPalette=primary and renders turquoise, so the rename
lands it on secondary — correct for what it draws. In both cases A changes
no pixel of anything anyone has already placed.
B — the bindings are wrong. Rebind each
cell’s fill and stroke to the opposite role. If you take this route you must{' '}
also move each set’s default, or the
default silently changes colour in every instance in every file — Button turning
turquoise, avatar turning navy. Avatar needs one extra edit either way: its{' '}
subtle / secondary cell has to leave{' '}
brand/primary/50 for a semantic role, which no rename can do.
tokens.css.
{' '}
That trades one wrong component for a whole kit that disagrees with the variable table it
is generated from — and tokens.css is machine-owned, so the next{' '}
figma-sync would overwrite the edit anyway.
variant=outline, colorPalette=secondary carries a visible{' '}
white fill (variant=outline, colorPalette=primary carries none. An outline button
should have no fill on either. Independent of §1 — fixing the palette crossing will not
fix this.
Every solid fill in the set was read together with the label drawn on it:
primary, red, green, yellow, black, third, So CBAR’s rule is: white on every solid fill, with yellow the single exception — the one place it darkens the ink instead. The kit had made that same trade in two more places, on the turquoise and the green, because white measures{' '} 2.07:1 and{' '} 2.28:1 there against the 3:1 WCAG asks for a control.
That deviation is now removed: .palette-secondary and{' '}
.palette-green set --ctl-solid-fg to{' '}
--ui-color-neutral-0. Fidelity to the design system was chosen over the
contrast number, deliberately and with the number on the record —{' '}
test/contrast.test.ts still measures both pairs and prints them, with a floor
of 1 so they no longer gate the suite. --ctl-fg, the ink every tinted
treatment uses, did not move and still clears 4.5:1. Consumer-facing note in README §9c.
The design-side finding stands: if CBAR wants
those two labels to pass AA, the fill has to move, not the ink. secondary-700{' '}
and green-700 both carry white comfortably.
Across all five treatments of the primary band, each state=active variant
paints exactly what its state=hover twin paints (2446:9576 solid,{' '}
2446:7384 subtle, 2446:7464 surface, 2446:7544{' '}
outline, 2446:7624 ghost). The kit has no :active treatment at
all, which lands on the same rendering. The showcase’s{' '}
Figma layout
{' '}
tab therefore draws both rows with the same forced class and says so.
Two hover tints do differ, and are left alone for now:
subtle and surface — CBAR’s hover is the same tint as its
default (step 100); the kit deepens one step to --ctl-subtle-hover (200), so
a tinted button here responds to the pointer and there does not.
outline and ghost — CBAR’s hover is step{' '}
50; the kit uses step 100 (
--ctl-subtle). The kit already has the right token for this,{' '}
--ctl-soft, which theme.css introduced for exactly this rung.
Switching the two transparent treatments over is a one-line change in{' '}
controlVariants — but it also moves Tabs and Pagination, which read{' '}
--ctl-soft today, so it wants its own decision.
disabled is a third: CBAR draws opacity: 0.3, the kit{' '}
0.5.
text/tertiary, the kit’s --ui-color-neutral-500 →{' '}
--muted-foreground. Who moves:{' '}
the design file, if anyone. The kit follows the ramp and should keep following it.
>
}
>
CBAR’s neutral ramp puts text/tertiary at
neutral-500
neutral-0
neutral-500
neutral-100
neutral-400
neutral-950
The second row is the one worth reading twice. Muted text on a muted surface is the{' '}
more common pairing in practice — table headers, code blocks, chips, every
secondary line inside a Card — and it is well under, not marginally under.
The third row is why this is filed as a light-mode finding rather than a token finding.
Dark mode drops --muted-foreground to neutral-400 and clears AA
outright, so the ramp is not the problem; the choice of step on a white page is.
The showcase’s own axe run — the Axe{' '}
switch in the toolbar, or ?a11y=on on any page — reports it on every page in
light mode and nothing in dark. After the five structural
fixes made while that panel was built, this token is the only remaining violation
anywhere on the site — which is the useful part: the panel is reporting a known trade, not
a regression, and a new finding there stands out.
test/contrast.test.ts carries all three rows. The two light ones have a floor
of 3 — enough to catch a real regression, low
enough not to gate the suite on a decision already taken — and the dark one has the full
4.5, because nothing about it is a compromise.
Nothing here is fixable in code without leaving the ramp, which is not worth doing for
0.09. The options belong to CBAR:{' '}
text/tertiary moves one step to neutral-600 (
text/tertiary is for large text and non-essential labels only and a second
variable covers body copy. Until one of those happens the kit renders what the ramp says.
--ui-shadow-xs … xl in tokens.css; CBAR’s{' '}
shadows/shadows light/* effect styles.{' '}
Who moves: the design file — it has no dark
elevation set to sync from.
>
}
>
Every one of the kit’s five shadows is painted in{' '}
oklch(0.145 0 0) at 5–10% alpha. That value is not merely dark, it is{' '}
exactly --ui-color-neutral-950{' '}
.dark sets{' '}
--background to. So in dark mode a raised surface casts a shadow in the colour
of the page it is sitting on, at one tenth opacity. It is not subtle; it is not there.
theme.css’s .dark block overrides forty-odd roles and none
of them is a shadow, so this is not an oversight in one component — the whole elevation
scale is light-mode-only. It is visible on the{' '}
tokens page
: switch to dark and the five elevation swatches become five flat rectangles.
tokens.css marks this block UNMAPPED{' '}
— CBAR’s file defines no elevation scale at all as a variable collection, and the
only shadow effect anywhere in it is a 1px inner shadow used as a border. What it does
publish is a set of effect styles named{' '}
shadows/shadows light/* — and the name is the finding. There is no{' '}
shadows dark/* beside it. The showcase’s own chrome uses those published
values (--cbar-shadow-* in showcase.css) and inherits the same
gap.
So a figma-sync run cannot close this. There is nothing on the other side to
read.
Design: publish a{' '}
shadows dark/* set. The usual answer is not a darker shadow — on a near-black
page there is no room below — but a larger spread at higher alpha plus a light 1px ring, so
the edge does the work the drop shadow does in light.
Kit: the convention CBAR already uses in light
is available and costs no new token. It tints its cards one step off the page rather than
relying on a shadow, which is why --card is a tinted surface here and not
white — and .dark already lifts --card to{' '}
neutral-900. Extending that lift to the popover/dialog layers would give dark
mode a real elevation cue in the file’s own idiom. Left undone deliberately: it is a
visual change to every floating surface, so it wants its own decision rather than a quiet
fix inside a parity write-up.
Alert (
2446:316); nodes 2446:592, 2446:597,{' '}
2446:587. Who moves: the design
file — the kit’s outline is border-only by definition.
>
}
>
Read in full while rebuilding the{' '}
Alert Figma-layout tab
. The headline is the good news, and it is the direct counter-example to §1: this set is
bound to the variables correctly. Its status axis is what the kit expresses as{' '}
colorPalette, and every band’s fill lands on the ramp step the variable
table says it should.
secondary],
['warning', yellow],
['success', green],
['error', red],
['info-secondary', primary],
['neutral', black],
]}
/>
Note what info is: the turquoise brand/secondary/500, while{' '}
info-secondary is the navy brand/primary/500. That is the same
pair §1 finds crossed on Button — and here it is the right way round, which is a third
witness (beside IconButton and ProgressCircle) that Button is the
single mis-wired set rather than the ramps being backwards. The showcase’s band table
was corrected to match; tokens.css was not touched.
variant=outline is supposed to be a border with nothing behind it, and on{' '}
info, error and neutral it is. On{' '}
warning (2446:592), success (2446:597)
and info-secondary (2446:587) it carries a{' '}
brand/primary/100, the navy tint. On{' '}
info-secondary that makes outline pixel-identical to{' '}
surface; on the other two it puts a blue wash under a yellow or green border.
It reads as three cells pasted from the surface row and re-bordered.
Not replicated. The kit’s outline stays{' '}
border-(--ctl-border) with no background at every palette, so the Figma tab
shows three cells that are lighter than the canvas. Same call as §2, which is the same bug
on Button.
The error border is red-600 warning’s subtle fill
is Yellow{' '}
collection surfacing again — the same one that put nine wrong steps in the kit before the
Variables became readable. Neither is worth a token change; both are worth knowing before
somebody eyedrops a value off this set.
Lastly, all 72 variants draw the same icon —{' '}
Outline/Status/Info-triangle — including success and{' '}
neutral. There is no INSTANCE_SWAP property to vary it, so a
consumer reading the set alone would think an alert has one icon. The kit takes the icon as
a child and imposes nothing, which is the right shape; the Figma tab draws{' '}
InfoTriangleIcon on all six bands purely to match the canvas.
Toast (
2414:10758); nodes 2414:10757, 2414:10759;
compared against Alert 2446:432 and Badge{' '}
2456:5339. Who moves: the design
file — the kit cannot satisfy both spellings at once.
>
}
>
Read while building the{' '}
Toast Figma-layout tab
. This set is the easiest to audit so far: getCSSAsync() returns the bound
variable names directly, so there is nothing to infer from hex values.
subtle-positive / colored-positive, success, 'exact'],
['error', subtle-negative / colored-negative, error, 'exact'],
['warning', subtle-warning / colored-warning, warning, 'exact'],
['info', subtle-secondary / colored-secondary, info, 'exact'],
['neutral', subtle-gray / colored-black, default, 'differs'],
]}
/>
info binds --surface-subtle-secondary — the turquoise again, the
third independent witness after §7’s info band and Badge’s{' '}
default colour that §1 is a Button-and-avatar defect rather than the ramps
being backwards. kindPalettes in toast.tsx already maps the first
four rows exactly, and the kit renders them byte-identical to the canvas.
The fifth row does not match, and the reason is not on the kit’s side. CBAR carries{' '} two variables for the same visual role, and different sets reach for different ones:
--surface-subtle-gray, --surface-subtle-gray,
The kit has one palette-black, whose --ctl-subtle is{' '}
--ctl-border --ctl-subtle at tokens.css is machine-owned — a{' '}
figma-sync would undo a hand edit anyway. The design file should collapse the
two variables to one; until then the kit stays on the value the majority of its own
components already render.
Geometry matches too, measured rather than assumed: 12px 16px padding, a 6px
radius, a 3px border on the leading edge only, 10px between icon and text, title
14/600 over description 14/28 with 5px between them, glyphs at 20px. Two differences are
the kit’s and both are deliberate — the canvas frames every toast at a fixed 306px
where the kit’s column is 384px and the toast fills it, and the kit adds{' '}
shadow-lg because a notification floating over live content has to read as
elevated where a flat swatch on a canvas does not.
Every variant’s left slot instantiates Solid/Status/Info-circle (
2163:653) — including success and error — and only
the fill changes. Checked on all five through the bridge rather than inferred from one,
and the close cross beside it takes the same colour:
green-600, '3.00:1', '6.18:1'],
['error', red-600, '3.65:1', '6.86:1'],
['warning', yellow-ink, '5.31:1', '5.31:1'],
['info', secondary-500, '1.74:1', '8.34:1'],
['neutral', neutral-800, '15.11:1', '15.11:1'],
]}
/>
Every one is already a named ramp step, which is what made adopting them a token change
rather than five literals. The close cross takes the same colour as the status glyph, so
the table covers it too. The last column is --ctl-fg, what the title and
description are set in and what the glyph used to use: on warning and{' '}
neutral the two agree byte-for-byte, and on the other three the glyph is now
the lighter of the two. info is the extreme — a turquoise glyph on a
turquoise surface at 1.74:1 is decorative rather than legible, and the
text beside it stays at 8.34:1.
Unlike Alert there is an INSTANCE_SWAP property (icon,
defaulting to that node), so the set can vary it and simply does not.
This one was adopted rather than recorded.{' '}
The kit used to pick a glyph per kind — a tick for success, a warning
triangle for warning, an octagon for error, and none at all for
a plain toast. It now draws CBAR’s single info-circle on all five and tells them
apart by colour, which is the design file’s own scheme. loading keeps
its spinner: it has no counterpart on the canvas, and a static circle would throw away
the one thing a pending toast has to say.
The ink moved with it, through a new --ctl-icon role in{' '}
theme.css bound to the ramp steps above. It is a lower-contrast role
than --ctl-fg and must never be used for text — that is the cost, and it is
the second time on this page that matching CBAR was chosen over a contrast number, after
§3. Both tabs of /toasts now render the same thing, and the Figma-layout tab
forces nothing at all: the two sides converged, which is the outcome a parity page exists
to produce.
iconRight? is a boolean defaulting true, so every variant on
the canvas carries a dismiss control. The kit’s{' '}
Toaster closeButton defaults false. Left alone on purpose:
flipping it would add a button to every toast in every app already consuming the kit, for
a difference that is one prop away. The Figma-layout tab draws it on to match the canvas
and says so.
Toast’s only axis is named state, and the parity report treats an axis
by that name as an interaction concern drawn in CSS — correct for the fourteen other sets
whose state holds hover / focused /{' '}
disabled, and wrong here, where it holds statuses. Toast was therefore being
reported with no comparable axis at all. Fixed with an explicit{' '}
statusAxes opt-out on the registry entry rather than a cleverer heuristic,
because Button already maps its own state axis onto a{' '}
disabled prop while genuinely being a CSS state.
ghost variants, but they are{' '}
visible: false and never render — getCSSAsync() reports
neither. Do not read a raw fills array without checking visibility.
colorPalette=third solid is --ui-color-tertiary-500 here. The ramp is right; only the name is
legacy (third is kept as a deprecated alias of tertiary for one
major — see README §9a-i).
Open the “CBAR Figma bridge” plugin in Figma, then from the project root:
Or, with no scripting at all: open{' '} the Button page {' '} and read the Figma panel at the bottom. It resolves the playground’s selection to the real variant and puts CBAR’s own render and computed style beside the kit’s — which is how §1 was found.
For §3 and §4 the faster route is the{' '} Figma layout tab : it arranges the live components the way the canvas arranges them — a band per palette, a column per treatment, a row per state — so the two can be compared cell for cell without resolving anything by hand.