Increased contrast
What prefers-contrast changes, what forced-colours mode does instead, and an honest account of the per-component answers that exist while no global one does.
What we guarantee
Today: nothing system-wide. This page exists because the gap is real and
should be visible, not because there is a global feature to describe.
app/globals.css implements prefers-reduced-motion and
prefers-reduced-transparency; there is no prefers-contrast block and no
site-wide forced-colors block. Several components now answer forced-colors: active on their own, and a grep of registry/bases/base for a forced-colors:
variant class shows the seven below:
Surfacedraws its edge as anoutlinerather than abox-shadow, so a browser keeps it and recolours it toCanvasTexton the four rungs that publish a border, and every card and tile built onSurfacestays bounded.Fieldswitches an invalid control's border to dashed, so the invalid state still reads once the browser has repainted that border to a system colour.Buttongives its primary variantHighlightandHighlightText, so the recommended action stays filled and its label meets the system pairing.ScoreDialandTrendSparklinemap their SVG strokes to system colours, because forced colours recolours boxes and text but leaves strokes where the author put them.RangeBarmaps its boundary tick toCanvasTextand its reading marker toHighlightinside aCanvasTextoutline, so the scale's edges and the current reading stay separable once the palette is replaced.Sheetmaps its scrim toCanvas, so the dimmed backdrop becomes the system background rather than a tint the browser would otherwise drop.
All of this lives in each component rather than in a stylesheet, and deliberately
so. The product stylesheet does not travel with a shadcn add install while the
component file does, so a rule that ships inside the component reaches the reader
who installed only that component.
What the system does provide that makes the gap survivable in the meantime:
- Every published token pair is measured and the results are on Contrast conformance, so the baseline is known rather than assumed.
- Colour is never the only carrier of clinical status, which is the property that matters most in a forced-colours environment where the browser will replace your palette with the reader's. See Colour independence.
- The colour engine can derive a higher-contrast variant of a ramp, because the roles are computed in OKLCH from lightness targets rather than hand-picked. That is what makes the intended implementation cheap.
What it will change
When it lands, @media (prefers-contrast: more) will do exactly four things, and
deliberately not more:
- Raise the lightness distance between ink and surface in every status and category pair, so each pair clears a higher floor than the standard one.
- Make every border explicit. Hairline dividers at low alpha become solid at full token colour; the material ladder's borders stop being decorative.
- Thicken the focus indicator and give it a solid outer contour so it survives on any surface.
- Disable translucency and blur, by reusing the reduced-transparency degradation. High contrast and frosted glass are incompatible requests.
It will not change type size, spacing, layout or the meaning of any colour. A reader asking for more contrast is not asking for a different product.
prefers-contrast: more and forced-colors: active are different mechanisms and
need different answers. The first is a request to your palette. The second
replaces your palette with a system one, and the correct response is mostly to
stop fighting it: keep borders declared, avoid conveying anything by background
colour alone, and use forced-color-adjust only where a colour genuinely carries
information that would otherwise be lost.
What you own
Two mechanisms, two answers. Forced colours is answered per component today, in
the files listed above, so a product built on opsinjs does not have to redraw the
boundary Surface already draws or the strokes ScoreDial already maps. What stays
the product's there is its own colours, its own information-bearing images, and any
boundary it draws with a shadow instead of a border, which are the three items
below. prefers-contrast: more is the other half, and it has no token-layer
escalation yet, so raising contrast on demand is still entirely the product's
responsibility until the block above lands.
These stay yours under both mechanisms:
- Any colour you introduce. Your brand accent, your chart series, your illustrations. The system can only escalate what it defines.
- Images and icons that carry information. In forced-colours mode a photograph is unchanged while everything around it is recoloured, which can leave it illegible against its new surroundings.
- Anything drawn with a shadow instead of a border. Shadows are removed in forced-colours mode. A card that reads as a card only because of its shadow becomes a flat run of text.
How to check
Emulate prefers-contrast: more
In Chrome or Edge developer tools, Rendering → Emulate CSS media feature
prefers-contrast. Expected result today: the token layer changes nothing,
because there is no @media (prefers-contrast: more) block in either stylesheet.
Surface and DisclaimerNote do answer the variant in their own utilities, so on any
page that renders a Card, Dialog, Sheet or DisclaimerNote you should see the
surface go opaque, lose its backdrop blur and gain a thicker edge. Seeing those
three changes and nothing else is the correct current outcome.
Turn on a Windows contrast theme
Settings → Accessibility → Contrast themes. This activates forced-colors: active
in the browser. Expected result: the site remains usable, all text is legible,
and every control still has a visible boundary. Failure looks like invisible
buttons, a status pill whose text and background have both become the same system
colour, and disappearing card edges.
Turn on the macOS and iOS setting
Settings → Accessibility → Display → Increase contrast. This drives
prefers-contrast: more in Safari. Repeat the primary task.
Measure again in the new state
Any pair the browser has altered needs re-measuring. The published figures describe the default themes only.
Measured results
scripts/check-contrast.mts. Run pnpm run generate and reload.Nothing on this site types a measured number by hand, so an ungenerated table shows this rather than an example.
There is no increased-contrast theme to measure. When the escalation lands, the contrast script will emit a third column for every pair because a high-contrast mode that has never been measured is a claim, not a mode. The three columns will be light, dark and increased-contrast.
Known gaps
This page is mostly gap, so the list is the substance rather than a footnote.
prefers-contrasthas no token-layer answer. No@media (prefers-contrast: more)block exists inapp/globals.cssorapp/product.css, so the escalation this page specifies is still the one that is missing. It is answered per component instead: Surface mapscontrast-more:to an opaque tint and a thicker edge in its own utilities, and DisclaimerNote thickens its rule under the same variant.forced-colorsis answered per component, has no site-wide block, and has not been tested on a real theme. Surface, Field, Button, ScoreDial, TrendSparkline, RangeBar and Sheet each handle it in their own file, but no site-wide@media (forced-colors: active)block exists and the site has never been opened under a Windows contrast theme.- There is no increased-contrast token set, so nothing can be measured.
prefers-contrast: lessis not considered at all, and we have no view on what it should do.
Implement the four-part escalation described above in the opsinjs token layer,
add a forced-colors pass that guarantees a visible boundary on every control,
and extend scripts/check-contrast.mts to emit measurements for the escalated
theme.
Updates to this page
Last read through against the system on 2026-09-02. Due for review every 3 months; expiry is reported by pnpm run check:freshness.
Reduced transparency
What each rung of the material ladder becomes when the reader has asked for less translucency. The layout must not move, and this page says why.
Cognitive accessibility
Reducing load, removing time limits and making every error recoverable is the accessibility work that matters most in health and is measured least.