DEV Community

Cover image for Why Dark Mode Tests Don’t Cover Windows Contrast Themes
Svg/icons
Svg/icons

Posted on

Why Dark Mode Tests Don’t Cover Windows Contrast Themes

A toolbar can look correct in light and dark mode and still become difficult to use with a Windows contrast theme.

The missing check is forced colors: a browser mode that applies a restricted, user-selected palette to the page.

For SVG icons, this raises a different question from ordinary theme support:

When the surrounding interface changes to system colors, does the icon remain recognizable—and does the control still look like a control?

This article explains the documented behavior and a practical way to check it. It does not present results from a completed Windows browser comparison.

Start with the rendered control

A contrast calculation evaluates a foreground against a background. But the colors you authored are not necessarily the colors the browser paints in forced colors mode.

A button might normally use:

  • a colored background;
  • a white icon;
  • a subtle shadow;
  • a different background for its selected state.

When the browser applies the user's palette, some of those distinctions can change or disappear.

The useful unit to review is therefore the whole control: its icon, label, boundary, focus indicator, and state.

An SVG can remain present in the DOM while becoming difficult to distinguish visually. A button can remain clickable while losing the visual cues that explain what it does.

What changes in forced colors?

The forced-colors media feature detects whether the browser is enforcing a restricted palette.

@media (forced-colors: active) {
  /* Targeted adjustments for the user's palette */
}
Enter fullscreen mode Exit fullscreen mode

MDN documents adjustments to text, backgrounds, borders, and SVG paint properties. It also documents the removal of box shadows and non-URL background images, including CSS gradients.

Two details matter for icons:

A URL-based SVG background is not automatically removed by the rule that removes gradients. Its artwork may still become difficult to see against the changed surface.

SVG has specific color-adjustment behavior. The CSS specification describes preserve-parent-color, which allows embedded SVG to receive its parent's adjusted text color while otherwise preserving artwork colors.

That makes blanket statements such as “forced colors recolors every SVG path” unreliable.

Keep monochrome action icons connected to text

For a monochrome icon inside a button, using currentColor for the appropriate fill or stroke lets the artwork follow the control's text color.

For example:

export function DownloadButton() {
  return (
    <button className="action" type="button">
      <svg
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        aria-hidden="true"
        focusable="false"
      >
        <path
          d="M12 3v12m-5-5 5 5 5-5M5 17v4h14v-4"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>

      <span>Download report</span>
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

The SVG does not introduce a separate text color. Its stroke follows the inherited color.

This example uses simple illustrative geometry. When adapting an existing icon, preserve the distinction between fill and stroke. Replacing every paint value indiscriminately can change the artwork.

For a broader explanation of integration methods, see Same SVG, Different Runtime.

The important reminder here is that an external SVG loaded through <img> does not inherit the surrounding button's text color into its internal paths. Adding currentColor inside that file does not create the same inheritance relationship as inline SVG.

Check whether the button boundary survives

An icon is only one part of the control.

If a button depends entirely on a shadow to separate it from its surroundings, removing that shadow can make the boundary unclear.

A transparent border reserves space in the ordinary design. A targeted rule can then give that border a system color:

.action {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;

  color: #17324d;
  background: #edf3f9;

  border: 1px solid transparent;
  border-radius: 0.375rem;
}

.action svg {
  flex: none;
}

.action:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

@media (forced-colors: active) {
  .action {
    border-color: ButtonText;
  }

  .action:focus-visible {
    outline-color: Highlight;
  }
}
Enter fullscreen mode Exit fullscreen mode

ButtonText and Highlight refer to the active system palette.

Treat this as a starting pattern to inspect in context. A default button, a disabled button, and a selected toggle have different communication requirements.

Don’t preserve the entire design with an opt-out

This rule may appear to solve unexpected changes:

.application {
  forced-color-adjust: none;
}
Enter fullscreen mode Exit fullscreen mode

But it disables automatic color adjustment throughout that subtree.

That can preserve the same color combinations the user enabled a contrast theme to avoid.

Keep any exception narrowly scoped. A color swatch may need to preserve a particular color because that color is the information. An ordinary toolbar usually benefits from following the user's palette.

If an exception is necessary, check both the preserved artwork and its contrast against the surrounding interface.

Test states, not just screenshots of the default theme

A useful review should answer specific questions:

Check Question
Icon Can its shape still be distinguished from the surface?
Label Is the action understandable if the icon is unclear?
Boundary Can users identify the clickable control?
Keyboard focus Is the focused control visibly indicated?
Selected state Can users distinguish selected from unselected?
Disabled state Is the unavailable state still recognizable?

For a toggle, exposing aria-pressed communicates its state to assistive technology. Sighted users also need a visible indication that survives the active palette.

Similarly, an accessible name on an icon-only button does not resolve an invisible icon for a sighted user.

Both forms of communication need review.

Use emulation during development, then check Windows

Browser emulation is useful for finding problems early.

You can confirm that the browser reports forced colors with:

window.matchMedia("(forced-colors: active)").matches;
Enter fullscreen mode Exit fullscreen mode

A true result confirms that the media query matches. It does not confirm that the interface is usable.

For a Windows-focused check, use actual Windows contrast themes as well:

  1. Open the interface in a browser you support.
  2. Enable a contrast theme with a dark canvas.
  3. Inspect the controls and navigate with the keyboard.
  4. Repeat with a contrast theme with a light canvas.
  5. Check selected, disabled, hover, and focus states where applicable.

Record the Windows version, browser version, theme name, and whether the test used OS settings or emulation.

Label screenshots accordingly. An emulated browser capture should not be presented as a capture of an actual Windows contrast theme.

Make an icon comparison reproducible

If you compare integration methods, keep the artwork constant.

Choose a small group of monochrome icons from one family and save:

  • the exact SVG files;
  • their source URLs;
  • the applicable license information;
  • any changes made to fill, stroke, or color;
  • the component code and test conditions.

Then compare inline SVG, <img>, and URL-based CSS backgrounds using the same geometry.

This separates differences caused by integration from differences introduced while preparing the assets.

A collection exported from SVGicons can provide the source assets, but the exported files are only the beginning. The component that renders them still needs inspection in the target environment.

Add forced colors to the review

Light and dark themes check the palettes your application provides.

Forced colors checks how the interface behaves with a palette chosen by the user.

Before shipping an icon-based control, make sure its meaning, boundary, focus, and state remain available under that condition too.


References

Top comments (0)