Forced colours mode is the setting behind Windows High Contrast. When it is on, the browser throws away most of your colour decisions and repaints the page in a palette the user chose: one colour for text, one for background, one for links, one for buttons. It is not a dark theme. It is a user saying "I will decide what things look like, thank you".
What makes it interesting to test is that the override is partial. The browser forces some properties and leaves others alone, and the ones it leaves alone are exactly the ones a drawn interface relies on.
Nakodo is a drawn interface. Its public pages contain no photographs and no raster images, just line art. So I turned forced colours on for all 76 pages and counted what changed.
Running it
Chrome can emulate the mode, which means you can test it without a Windows machine:
await page.cdp("Emulation.setEmulatedMedia", { features: [{ name: "forced-colors", value: "active" }] }); By hand it is DevTools, the Rendering panel, "Emulate CSS media feature forced-colors", set to active. Readers can follow along on any page of the site.
Then on each page I walked every element and recorded its computed color, backgroundColor, fill and stroke, and whether each had become a system colour or kept its own.
The counts
Across 76 pages:
| Measure | Count |
|---|---|
| Elements | 59,700 |
| of which SVG elements | 34,175, or 57 per cent |
color values replaced by the system palette | 57,607 |
color values that kept their own value | 2,093 |
| SVG strokes untouched | 23,733 |
| SVG fills untouched | 15,884 |
Declarations of forced-color-adjust on the site | 0 |
The first line of the result is good news. Text is the thing forced colours is for, and 57,607 of 59,700 elements hand their text colour straight over. Paragraphs go white, links go yellow, buttons become outlined boxes. The pricing page in this mode is entirely readable: three plan cards as black boxes with white borders, every feature line legible, every price legible.
The one thing it loses there is emphasis. In normal colours the recommended plan is an inverted dark card that pulls your eye across the row. In forced colours all three cards are identical, and the only reason you can still tell which one is recommended is that the word "RECOMMENDED" is text rather than a colour. That is the rule this mode teaches, and it is worth writing on a wall: any distinction carried by colour alone does not survive, and any distinction also carried by a word or a shape does.
The 57 per cent the mode does not touch
More than half of every page here is SVG, and forced colours mode does not override fill or stroke. That is per spec, not a Chrome bug: the forced properties are things like color, background-color, border-color and outline-color. Paint on a shape is left to the author.
So 39,617 colour values across the site carry on exactly as they were, on top of a background the user has just changed to black. Whether that is fine or catastrophic depends entirely on which colour each shape was drawn in. Measured against the forced background:
| Drawing colour | Contrast against the forced canvas |
|---|---|
| paper 0 (the fill inside drawings) | 20.63:1 |
| ochre 400 (the spot colour) | 11.46:1 |
| cyano 800 (the brand mark) | 2.23:1 |
| ink 900 (every outline) | 1.33:1 |
The top two are better in forced colours than they are in daylight. The bottom two are the problem, and the bottom one is the whole line system.
Every outline in every drawing on the site is --illo-line, which is ink 900, which lands at 1.33:1 against black. A line at 1.33:1 is not a faint line. It is not there.
What saves the screenshots from being empty is that the drawings are not outlines alone. They are shapes filled with paper and spotted with ochre, with ink outlines on top. The birds on the home page are still perfectly clear in forced colours: white bodies, ochre wings, and outlines you cannot see but do not need. The autumn leaves in the hero, which are mostly ink with a little ochre, are mostly gone.
The clearest single failure is the brand mark in the header. It is one path filled with var(--primary), which is cyano 800, which is 2.23:1 against the forced background. That is below the 3:1 that WCAG 1.4.11 asks of a meaningful non-text graphic, and it sits in the top left corner of all 76 pages.
The one thing that was already handled
There is exactly one rule in 154 KB of CSS that anticipates this mode:
@media (forced-colors: active) { .hatch { background-image: none; border: 1px dashed CanvasText; } } Hatching means "locked" or "not recorded yet" in this interface. It is a repeating background image, and forced colours drops background images, so the meaning would have gone with the pattern. The fallback turns it into a dashed border instead.
It works. Measured on the home page in forced colours, the hatched elements report background-image: none and a 1px dashed border in the system text colour, and in the screenshot the hatched avatar circles are dashed rings exactly as intended.
It is also the only public page that has a .hatch element on it, which is a good reminder of how these things go: the one rule somebody wrote for this mode protects the one component they were thinking about at the time.
The fix is three lines
This is where an earlier decision paid off. Every drawing on the site reads its colours from three custom properties rather than hardcoding them:
:root { --illo-line: var(--color-ink-900); --illo-fill: var(--color-paper-0); --illo-spot: var(--color-ochre-400); } There are already two overrides of that trio in the stylesheet, one for the dark theme and one for inverted panels, and each is three lines. So the forced colours version is three more, using the system keywords that are the whole point of the mode:
@media (forced-colors: active) { :root { --illo-line: CanvasText; --illo-fill: Canvas; --illo-spot: CanvasText; } } That maps every outline on the site to the user's text colour and every fill to the user's background, which is precisely what they asked for. The brand mark needs one more rule of its own, since it is painted with --primary rather than with the drawing variables.
Worth saying: forced-color-adjust: none would also make the drawings "work", by exempting them from the mode entirely. That is the wrong tool here. It is for things like a colour picker, where the colours are the content. A kingfisher is not content, it is decoration, and decoration should do as the user says.
Try it
Open any page on nakodo.app, open DevTools, Rendering panel, set forced-colors to active. Then this will tell you how much of the page the mode did not reach:
[...document.querySelectorAll("body *")] .filter((el) => { const cs = getComputedStyle(el); const sys = /^rgba?\((0, 0, 0|255, 255, 255|255, 255, 0)/; return (cs.stroke && cs.stroke !== "none" && !sys.test(cs.stroke)) || (cs.fill && cs.fill !== "none" && !sys.test(cs.fill)); }).length; The home page returns the largest number, because it has 860 SVG elements on it. The glossary returns one of the smallest. Neither of them will return zero until I ship those three lines.
The short version
- Forced colours is mostly a text feature, and text came through fine. 57,607 of 59,700 colours replaced, no layout damage, every page readable.
- SVG paint is yours to handle.
fillandstrokeare not forced, so more than half of this particular site ignored the mode, and only the lightness of the fills kept it legible by luck. - Measure contrast against the forced background, not against your own. Ink on paper is 15.74:1 and the same ink on the user's black canvas is 1.33:1. It is the same colour. It is a different page.
- A theme that runs on a few variables is cheap to extend. Three lines, because somebody avoided hardcoding ink into 26 drawings two months ago.
