Dev.to WebDev 🛠 Dev 👁 0 📖 7 min read

A map that doesn't need colour: our embed in Windows forced-colors mode

We're the team behind Regime Map, a country-by-country register of gambling regulation. Our embeddable map colours each country by its regulatory status. A few days ago we wrote about making that embed survive any host p

A map that doesn't need colour: our embed in Windows forced-colors mode

We're the team behind Regime Map, a country-by-country register of gambling regulation. Our embeddable map colours each country by its regulatory status. A few days ago we wrote about making that embed survive any host page. Under that post, a reader asked something we hadn't tested: what happens to the map in Windows forced-colors mode?

The honest answer was that it broke. This post is how we fixed it.

The world map switching from normal colours to a dark, then a light, contrast theme.

What a contrast theme does to a map

In a Windows contrast theme the browser repaints every page with a small palette of about eight system colours: one for the background, one for text, one for links, one for buttons, and so on. For text, that is exactly what a reader with low vision or light sensitivity wants.

Our map has six statuses, each with its own fill: licensed regime, state monopoly, varies by region, prohibited, no licensing regime and not assessed, plus "not covered yet" for countries we haven't checked. The legend under the map is the key.

In forced colours the legend lost its key: every swatch went blank, so the map was still coloured but nothing said which colour meant which status. And the map ignored the reader's theme: with the default embed code, in a dark contrast theme it sat on the page as a bright block of our own colours, exactly the glare the reader had switched the theme on to avoid. (An embed set to its dark theme kept its dark palette instead; its legend was just as blank.)

Both have one cause. The browser repaints CSS backgrounds, so the swatches lost their colours. Chromium doesn't repaint SVG fills, so the map kept ours.

The world map in a dark Windows contrast theme, before and after the 4 November 2026 update. Before: a bright block of the map's own colours, and a legend with no colour marks. After: black-and-white textures in the reader's theme colours, with the same textures in the legend.

Who sees it, and who turns it on

Not us. A reader turns it on in Windows 10 or 11 (Settings → Accessibility → Contrast themes), and Edge, Chrome and Firefox all follow it. Firefox can also be set to override the colours a page specifies. macOS "Increase contrast" doesn't trigger it.

The page sees a single media feature:

@media (forced-colors: active) {
  /* … */
}

So there's no option in the embed code and nothing for a host site to configure. The script form and the iframe form behave the same: the embed follows the reader's system.

Textures instead of colours

The fix had to work inside the reader's theme: no colours of our own, only the system's handful, and that isn't enough for six statuses. So something other than colour has to carry the status. Each status got its own texture:

status texture
Licensed regime solid
State monopoly dots
Varies by region diagonal hatch
Prohibited cross-hatch
No licensing regime horizontal lines
Not assessed vertical lines
Not covered yet empty, dashed outline

On the map they're SVG <pattern>s drawn in CanvasText on Canvas, so they invert correctly between dark and light themes. In the swatches, the same textures are CSS gradients. A lawful exception, such as pachinko halls in Japan's land-based gambling, keeps the asterisk pattern it already had, and it's spelled out in words in the tooltip, the table and the card.

The map legend in normal colours and in two contrast themes: seven statuses, seven textures.

Here is the "prohibited" pattern, slightly simplified (the renderer builds this markup as a JavaScript string and writes the transform at draw time). Two crossing bars, rotated 45°, give diagonal lines both ways:

<pattern id="rmefc-pro" width="6" height="6" patternUnits="userSpaceOnUse"
         patternTransform="rotate(45)">
  <rect width="6" height="6" fill="Canvas"/>
  <rect width="1.25" height="6" fill="CanvasText"/>
  <rect width="6" height="1.25" fill="CanvasText"/>
</pattern>

The system colours sit in presentation attributes, and one rule in the forced-colors block points each country at its pattern:

@media (forced-colors: active) {
  .map [data-s=prohibited] { fill: url(#rmefc-pro) !important; }
}

System colour keywords, and nothing else

Inside the forced-colors block there are no hex values at all, only CSS system colour keywords:

@media (forced-colors: active) {
  /* Canvas, CanvasText, GrayText, Highlight,
     LinkText, ButtonFace, ButtonText. No hex. */
}

The reader's theme decides what those look like. In Chromium's dark emulated palette, GrayText comes out green and links yellow; in the light one, GrayText is dark red. We don't need to know. There is one such block per stylesheet: the map's, the card's, and the <link> fallback for browsers without constructable stylesheets.

Opting out, only where it's needed

In forced-colors mode the browser takes over every colour by default, and for almost everything that's what we want: text, buttons, links, borders, the tooltip. We leave those alone.

Two things opt out: the map's SVG, and the status swatch, one small element reused in the legend, the table, the tooltip, the bottom sheet, the search list and the card. These are the real rules, trimmed (sizes dropped, one of the six swatch textures shown; .xsw is the legend's small asterisk key):

@media (forced-colors: active) {
  .stage svg.map, .rme .xsw { forced-color-adjust: none; }
  .rme .sw { forced-color-adjust: none; border: 1px solid CanvasText; background: Canvas; }
  .rme .sw--prohibited { background:
    repeating-linear-gradient(45deg, CanvasText 0 1.25px, transparent 1.25px 6px),
    repeating-linear-gradient(135deg, CanvasText 0 1.25px, transparent 1.25px 6px), Canvas; }
}

The swatches are tiny CSS boxes whose textures are background gradients, and forced colours removes background images, so without opting out they go blank. The map's SVG opts out too; in Chromium that turned out to be only a safeguard (its pattern fills survive either way), but other browsers treat SVG differently.

Having opted out, they do the browser's job themselves: they draw only in system colours. Opting out and then painting in your own brand colours would undo the reader's theme.

The rule from the first post still holds: there is no style="" attribute anywhere, so a host with a strict style-src-attr still renders the map. Sizes go through the CSSOM or SVG attributes, never a style attribute, and the pattern tiles are rescaled at every zoom level so they stay 6 × 6 pixels on screen. This is the real line (ES5); S is screen pixels per map unit:

x.setAttribute('patternTransform', 'scale(' + (1 / S).toFixed(5) + ')' + (r ? ' rotate(' + r + ')' : ''));

Bigger dots, focus in Highlight

Under forced colours only, the small-state dots grow from a radius of 4 to 6 at world scale and get a Canvas halo, so they stand out from the sea. Keyboard focus and the selected country are drawn in Highlight, the colour the reader's theme uses for "this one".

The Europe view in a dark Windows contrast theme, with small states drawn as larger textured dots.

If the reader switches a theme on or off with the page open, a matchMedia('(forced-colors: active)') listener redraws the map.

Two browser quirks

  • A transparent border is forced visible. Our inactive product tabs had transparent bottom borders. In forced colours every tab grew an underline, and you couldn't tell which one was active. The inactive tabs now use a Canvas border, which blends into the background as intended.
  • Chromium paints a "backplate" behind text. In "Find a country", the active match had a filled background, and the backplate hid its own label. The active option is now an outline instead of a fill.

Testing without a Windows machine

We tested in Chromium's forced-colours emulation, light and dark palettes. In Playwright:

await page.emulateMedia({ forcedColors: 'active', colorScheme: 'dark' });

and the same with colorScheme: 'light'. By hand, in Chrome: DevTools → Rendering → "Emulate CSS media feature forced-colors". Chromium's two emulated palettes are those of Windows' classic "High Contrast Black" and "High Contrast White"; the dark one is close to Windows 11's "Night sky".

We went through 21 states in both palettes: the world map for two products, Europe with the small-state dots, zoomed in once and twice, the narrow product menu, search, a tooltip with an exception, the bottom sheet on a phone, keyboard focus on a country and on a dot, the table view, the credit bar, the country cards, and the iframe forms.

The world map in a light Windows contrast theme, each status drawn as its own texture.

The other half of the test mattered as much: had we changed the normal look? We screenshotted the same 21 states, light and dark, with the old renderer and the new one, both on the same production data, and compared them pixel by pixel. 38 of the 42 pairs were identical. The four keyboard-focus states differed by 1,710 to 2,570 pixels of anti-aliasing along borders, and the old renderer compared with itself differs by the same amounts.

What it cost

1.7 KB gzipped for the map renderer and 0.4 KB for the card. The whole map (loader, renderer and data) is now about 47 KB gzipped, and a country card about 8.5 KB.

What we haven't done yet

  • A real Windows machine. Everything above is emulation. Windows 11's other contrast themes (Aquatic, Desert, Dusk) use other colours. The textures, layout and wording stay the same, but we haven't seen them on the real thing. Nor have we tested Firefox's own "override the page's colours" setting.
  • The closest calls. At world scale, a state-monopoly dot and a prohibited dot are four dots in a ring against an X in a ring: distinct, but small. A country only a few pixels across, like Luxembourg or Cyprus, has no room for a 5–6 px texture until you zoom in, as in the normal look. And "not covered yet" differs from the sea only by its outline.

If you use a contrast theme, we'd like to hear what you see: in the comments, or at [email protected].

Try it

The configurator, with a live preview of the map and the country cards, is at regimemap.com/data-api. The Hugging Face demo loads the live embed, so it already shows all of this: open it, turn on forced-colors emulation in DevTools and look.

Last time we promised a post on counting embeds without logging the people who read them. That one comes next.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.