An accessibility token layer that survived a UI rewrite
The accessibility floor is written by redefining tokens inside media queries. That only keeps working if every Tailwind utility emits a var() reference rather than a compiled copy.

A design token layer survives a UI rewrite when the new styling system references the tokens instead of copying them. In the Market4 panel the accessibility floor is implemented by redefining tokens inside media queries: under prefers-contrast, prefers-reduced-transparency and prefers-reduced-motion, the same names take stronger values, and every consumer is corrected at once. Moving to Tailwind kept that working for one reason. The theme is mapped with @theme inline, which emits a var() reference, rather than with a plain @theme block, which would compile each token's value into each utility at build time.
A plain Tailwind theme mapping copies the value at build time. The utility ships as border-color: rgb(15 23 42 / .09) and is deaf to the media query for ever after. Nothing errors, no build fails and no test turns red: the panel quietly stops honouring the accessibility floor on the day a screen grows a Tailwind class. A mapping written as @theme inline emits .border-hairline { border-color: var(--hairline) } instead, so the media query keeps reaching it, and so does dark mode, which is implemented the same way.
This is the failure mode that costs the most to find, because the symptom is an absence. Nobody files a bug that says "the high-contrast setting stopped changing anything", and a screenshot of the panel looks correct in every state a developer normally opens. The class of bug is familiar: a CSS variable that resolves to nothing produces a declaration the browser drops rather than an error anyone sees.
| Mapping | What the utility ships as | Does prefers-contrast reach it? |
|---|---|---|
| @theme { --color-hairline: var(--hairline) } | border-color: rgb(15 23 42 / .09) | No. The value was resolved at build time. |
| @theme inline { --color-hairline: var(--hairline) } | border-color: var(--hairline) | Yes. The reference resolves per media state. |
The floor is a section at the end of the stylesheet, placed last so that it wins, and it changes token values rather than component rules. Under prefers-contrast: more, hairlines gain real weight, the secondary ink moves toward full ink, and the meaning colours darken. The reason the meaning colours have to move is specific: that block also lightens the inset-well surface, and against the lighter ground the stock inks fell under the very floor a high-contrast pass exists to raise. Darkened, they clear it again on both surfaces.

The link measured 297 by 44 pixels and reported a pass. Tapping its centre hit the paragraph behind it. An inline box that wraps is not one rectangle, and the pseudo-element meant to enlarge it was positioned against the same broken geometry.

Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
That single token carries the rule most often broken by accident in a dashboard. A ratio of 3.1:1 clears the floor for a user interface component and does not clear the 4.5:1 required for body text, so it belongs to axis ticks, disabled glyphs and decorative icons, and never to a sentence somebody has to read. The failure happens when a caption looks heavy and the nearest light grey is reached for, which is exactly why the value is named and documented rather than chosen per component.
Under prefers-reduced-motion: reduce, the panel collapses its duration and distance tokens to zero rather than deleting the feedback. Three things happen. Transforms and animations stop; the press-scale token becomes 1 so that button feedback survives as the colour half of the same rule; and one opt-in attribute keeps a duration, because an opacity cross-fade is the replacement for movement and has to stay visible. The tokens are collapsed centrally rather than each consumer checking the preference, which is what reaches the JavaScript motion layer as well, since it reads those durations through getComputedStyle.
Collapsing distances also protects against a classic reduced-motion failure. A reveal animation whose start state is opacity 0 leaves a blank page if the tween never fires. With the rise and stagger tokens at zero the element is already where it will end up, so a missed tween is invisible rather than fatal. The preference is answered by making the destination the starting point, not by hoping every animation checks the setting.
One end-to-end test, and it is written to fail for the right reason. It injects two elements side by side into a real, already-styled page: one styled with Tailwind classes, the other a bare element whose inline style reads the same raw tokens through var(). In each media state it asserts two things: that the two elements agree, so the utility is a reference rather than a copy, and that the value moved from the previous state, so the first assertion is not satisfied by a token that never changes. Either half alone passes on a broken build.
The probe's classes are safelisted deliberately, so the test cannot pass because some unrelated screen happened to use the same utility. And the probe is injected into a shipping page rather than into a route of its own, because the point is to measure the stylesheet the product actually serves.
By keeping the name as an alias until nothing reads it. When the panel's visual direction changed, retired names stayed in the stylesheet pointing at their replacements, because deleting one would have left the older CSS modules with a declaration whose value is empty and therefore no background at all. Five of those aliases were removed in one edit months later, once a search found no consumer left for any of them. Redefining a token retires a look everywhere at once; deleting a token retires it in the files you remembered to check.
Because restyling is per component and redefining is per value. A token redefined inside a media query corrects every consumer at once, including files nobody opened during the change, and no component can forget to participate. Restyling components means the accessibility pass is only as complete as the list of components somebody remembered, and that list goes stale the first time a new screen is added.
It emits a var() reference where a plain block emits a resolved value, so the browser resolves the custom property at paint time instead of the build resolving it once. In exchange, dark mode, prefers-contrast and prefers-reduced-transparency all keep working through the utility, and the token layer stays the one place a value is defined. For a dashboard that already relies on custom properties everywhere, the reference is the behaviour you want.
Render a real page under the emulated preference and compare computed styles, not source. A useful test asserts two things at once: that a component styled through the new system matches a bare element reading the raw token, and that the computed value differs from the value in the default state. The first proves the reference exists; the second proves the assertion is measuring something that moves.
No. It means removing motion, not feedback. Collapse movement and duration tokens to zero, keep a short opacity cross-fade as the replacement where an element genuinely needs to announce itself, and keep non-motion feedback such as a colour change on press. The setting is a request not to be moved, and answering it by deleting every visible response leaves an interface that feels unresponsive instead of calm.
A screen-reader-only caption kept its padding after being hidden, and the invisible node measured 32 by 16 pixels instead of 1 by 1. The cause was not the hiding technique. It was two selectors of equal weight and the order they were imported in.

A div with overflow auto and no tab stop is content a keyboard user cannot reach at all. Making it focusable is half the fix; an unnamed focusable region is announced as a group with no label. What to add, in what order, and why a rewrite is where this gets lost.