Back to blog list

High Contrast Mode: a complete guide for developers

About 4% of Windows users browse in High Contrast Mode. Learn which CSS properties the browser throws away and fix your UI with 8 practical tricks.

14 min read

Table of contents

The bug your team cannot see

A customer can complete a purchase on your site and be charged for something they never selected without any errors or logs on your side. Sounds abstract?

Let's take the delivery step of your checkout for example. Imagine three options as clickable cards saying "standard", "express" and "next day". The real radio input is hidden the way most design systems hide it, and the chosen card is marked with a fill. On a current copy of Windows with one accessibility setting switched on, all three cards become impossible to differentiate. The customer clicks on one of the shipping method and sees no change anywhere on the page.

So the order goes through on whichever option was actually selected. Nothing failed. The page rendered, the click registered, the payment cleared, and the first person to find out is whoever picks up the complaint two days later.

It is hard to spot, and if anyone on your team does spot it, they will wonder whether it is even worth fixing. On a "normal" browser nothing looks broken after all and no automated tool caught it either. So it is worth fixing?

What ignoring this costs you

Open your analytics and find the Windows share of your checkout traffic. Take 4% of that. On a typical European B2C split of roughly 30% Windows on desktop, the floor is near one in every 800 sessions. Most accessibility work starts with no number at all. This one you can size before you create a first ticket.

The 4% comes from Microsoft, which puts High Contrast usage at roughly that share of Windows users. In WebAIM's low vision survey, half of respondents said they use it. That second figure is the one that matters if your product serves an older or vision-impaired customer base.

One session in 800 is the optimistic reading. A customer who cannot finish a checkout does not come back next month, so you lose their whole future spend, not one basket. Additionally, the average is a lie for most industries. Pharmacies and insurers see these users far more often than a gaming store does.

That is a bigger group than the one your team already builds for. Screen reader users are a fraction of a percent of traffic, and nobody argues about supporting them. High Contrast Mode gets skipped because it is not a direct WCAG violation, not because it is rare.

The people behind that number are not all severely vision-impaired:

  • Users with cataracts or glaucoma, where reduced contrast sensitivity makes soft grey-on-white unreadable.
  • Users with photophobia and migraine, for whom a white page is physically painful.
  • Neurodivergent users who read faster on a controlled, predictable palette.
  • Field staff on a laptop in direct sunlight, who turn it on for one hour and turn it off again.

Then there is the legal side. The European Accessibility Act has been enforceable since 28 June 2025 in all 27 member states, and it covers e-commerce, banking, transport ticketing, and e-books sold to EU consumers. National penalties reach six figures.

Be precise about what the law asks, though. The EAA sets functional requirements and leaves the detail to EN 301 549, and that standard hits High Contrast Mode from two directions.

Clause 9 hands you to WCAG 2.1 AA. No WCAG success criterion mentions High Contrast Mode. What an auditor checks is 1.4.1 Use of Color, 1.4.11 Non-text Contrast and 2.4.7 Focus Visible. A button that loses its only visual boundary, or a focus ring that vanishes, fails those three here the same way it would anywhere else.

Clause 11.7 goes further, and this is the one teams rarely read. It says that software which is not isolated from its platform "shall follow the values of the user preferences for platform settings for: units of measurement, colour, contrast, font type, font size, and focus cursor". For a web page, the platform is the browser. High contrast mode is how the browser passes on the contrast preference the user set in Windows, and in EN 301 549 clause 11.7 is listed as applying to web content.

One caveat, because it matters for how hard you argue this. Whether 11.7 should apply to websites at all is an open question inside ETSI, and the draft V4.1.0 from November 2025 proposes limiting it to desktop and mobile apps. Build the business case on 1.4.1, 1.4.11 and 2.4.7, which nobody disputes. Treat 11.7 as the reason a regulator can ask for more than a contrast checker can prove.

So High Contrast Mode is not a separate legal requirement but the environment where your existing WCAG failures stop being theoretical, because the styling that hid them is gone.

Radio group with three shipping methods looking the same. One of them is selected but we don't know which one.
Example of a radio group failing Windows High Contrast Mode. The second shipping has been chosen but because it depends on color alone, the state change is invisible to the user.

What the browser actually does

High contrast mode gets mistaken for dark mode, or for a filter laid over your CSS. It is neither. The browser throws your color decisions away and substitutes a palette the user picked in their operating system. There are three consequences to this.

  • The user owns the palette, not you. Windows ships Aquatic, Desert, Dusk and Night sky, and every one of them is editable down to the individual color.
  • Colors come from native semantics, not from ARIA. A real <button> element gets ButtonText. A <div role="button"> gets treated as ordinary text, because the High Contrast Mode engine reads the tag, not the role.
  • The swap happens per property, not per element. Only a fixed list of properties is touched, and everything off that list renders exactly as you wrote it.

So the fix is never "restyle the component". It is knowing which properties get replaced with system colors:

  • color and background-color
  • border-color and outline-color
  • text-decoration-color
  • column-rule-color
  • and SVG fill and stroke

These get forced to a fixed value no matter what you wrote:

  • box-shadow and text-shadow become none
  • background-image becomes none for gradients
  • color-scheme becomes light dark, and scrollbar-color becomes auto

Everything else survives untouched. Layout, spacing, border-width, border-radius, font-size, transforms, opacity, and every url() image. What you lose is every boundary, state, and emphasis you drew with just color.

Your colors are replaced with these

When the browser needs a color, it takes one from a set of system colors that from the user's chosen theme. You can use these keywords yourself, and when styling for the High Contrast Mode, you almost always should.

  • Canvas and CanvasText. Page background and body text. Your default pair.
  • ButtonFace, ButtonText and ButtonBorder. Control surfaces, control labels, control borders.
  • Field and FieldText. Input backgrounds and the text typed into them.
  • LinkText, VisitedText and ActiveText. The three link states.
  • Highlight and HighlightText. Selected items. Useful for an active tab or a chosen option.
  • GrayText. Disabled state. The only reliable way to render something as unavailable.
  • Mark and MarkText. Highlighted text, matching the <mark> element.
  • AccentColor and AccentColorText. Accented controls, matching the system accent.

Be careful about older keywords. MDN deprecates a long list including Window, WindowText, ButtonHighlight, ButtonShadow, Menu, MenuText, InfoBackground and ThreeDFace. If you find these in an old stylesheet, replace them. For example, Window becomes Canvas, and WindowText and MenuText become CanvasText.

Most of them come in pairs. Keep the pairs together. Canvas with CanvasText, ButtonFace with ButtonText, Field with FieldText, Highlight with HighlightText. Those are the combinations the person editing the theme actually looked at, and a Windows contrast theme only exposes six swatches, so there are not many of them. Avoid crossing the pair, for example putting FieldText on ButtonFace, as it might result in inaccessibility for people with custom themes.

GrayText is the awkward one. It has no documented partner, because in the Windows theme editor it is the "Inactive text" swatch, chosen against the page background. So use it on Canvas and expect it to work. If you use it on ButtonFace or Field it is a risky decision.

8 tricks for High Contrast Mode

The fixes below are ordered by how much they can fix your design. The first one alone will repair most of a design system.

Trick 1: The transparent border

The browser replaces border-color, and transparent is a border color. So a transparent border is invisible in a normal browser. In High Contrast Mode however, it is a system-colored, one-pixel outline. This trick works in every browser that supports High Contrast Mode.

Modern buttons frequently are based on background-color alone. If you strip the background, you are left with a word floating in space, with nothing to say it can be clicked.

Example: A button that keeps its shape
CSS

@media (forced-colors: active) {
    button, [type="button"] {
        border: 1px solid transparent;
    }
}

Example: Your primary button is a purple fill with white text and no border. In High Contrast Mode the fill becomes ButtonFace which is the same colour as the page background in every stock Windows theme. The button is still there, still clickable, still 44 pixels tall. It just no longer looks like a button. Adding one transparent border fixes the problem immediately.

Trick 2: Focus rings that survive

If your focus ring is based on a box-shadow, it does not exist in High Contrast Mode. box-shadow is forced to none with no exceptions and no media query that brings it back. This is a direct WCAG 2.4.7 failure and it is one of the most common problems we find.

How to fix it?

Use outline with transparent color. Additionally, use outline-offset to create some breathing room. It is one of the properties that survive untouched.

Example: Focus that survives High Contrast Mode
CSS

button:focus-visible, [type="button"]:focus-visible {
    box-shadow: 0 0 6px 2px #A78BFA;
    outline: 2px solid transparent;
    outline-offset: 2px;
}

Example: A design system sets outline: none on every interactive element and replaces it with a branded box-shadow glow. In normal browsing the focus indicator looks excellent. In High Contrast Mode a keyboard user tabs through the entire checkout with no visible focus at all, because the shadow gets completely removed. Transparent outline fixes that issue.

Trick 3: Icons that inherit

An SVG with a hardcoded black fill disappears against a black background. The High Contrast Mode swaps SVG fill and stroke, but only when it can tell what the icon belongs to.

How to fix it?

Use fill="currentColor" or stroke="currentColor" on inline SVG. The icon inherits whatever color the system assigned to the surrounding text. Keep in mind that the color of the surrounding text might be different based on the context. To make sure it is inherited properly, use ButtonText inside a button and LinkText inside a link.

Example: The markup for an icon that follows the theme
HTML

<button class="button" type="submit">
    <svg class="icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
        <path d="M6 5h15l-2 9H8L6 5Zm2 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm9 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" />
    </svg>
    Add to cart
</button>

The icon is aria-hidden because the button label already names the action.

Example: The styles that back it up
CSS

svg {
    fill: currentColor;
}

@media (forced-colors: active) {
    button, [type="button"] {
        fill: ButtonText;
    }

    a {
        fill: LinkText;
    }
}

Example: A cart icon in the header is a black SVG file. The user runs the Night sky theme, so the header background becomes near-black. The icon is still loaded, still positioned, still clickable but completely invisible. After adding stroke: currentColor to that SVG it turns white by itself on Night sky theme.

Trick 4: Images and gradients

A gradient written as background-image gets removed, but a raster or vector image loaded through url() is kept. There is one place Chromium and Firefox browsers split and that is border-image. Chromium keeps both real images and gradients in it, Firefox drops both. Test in both Firefox and Chromium browsers to make sure you support High Contrast Mode in all environments.

How to fix it?

  • Put an image in an <img> tag with real alt text if the image carries meaning.
  • If you ever build a UI boundary out of a gradient like a divider or a gradient-filled bar, add a transparent border alongside it.
  • Your decorative gradients will disappear in High Contrast Mode. That is the correct outcome, and you should not treat it as a bug.
Example: A divider that does not vanish
CSS

hr {
    height: 2px;
    background-image: linear-gradient(90deg, #6D28D9, #DB2777);
}

@media (forced-colors: active) {
    hr {
        height: 0;
        border-top: 2px solid CanvasText;
    }
}

Example: A progress bar on a multi-step form fills with a gradient and carries no text. In High Contrast Mode the fill is dropped and the track is left empty at every step, so a user four screens into an insurance quote has no way to tell how much is left. If you can't afford a "Step 4 of 6" label which is probably the best UX fix, the border on the filled portion is what you should go for.

Trick 5: Do not fight the text backplate

When text sits on top of an image, browsers draw a solid rectangle behind it, called a backplate. It exists because url() images survive High Contrast Mode while text color does not. Your carefully chosen white heading becomes CanvasText, the photo underneath stays exactly as bright as it was, and without the backplate the two would collide.

It is not a bug and you should leave it. The only way to switch it off is forced-color-adjust: none, disables user color preferences for that element and in majority of cases is considered an anti-pattern. If the backplate bothers you, give the text its own background and it should not appear anymore.

Example: A hero that does not need the backplate
CSS

.hero {
    background-image: url('/hero.jpg');
}

@media (forced-colors: active) {
    .hero__panel {
        background-color: #FFFFFF;
        border: 1px solid transparent;
    }
}

Trick 6: State is more than just a color

Things like active tabs or selected rows are indicated by color alone in most design systems. It is either a fill, a border or just a shade of text. From the High Contrast Mode perspective, they all use the same two or three system values, so the state stops being visible.

How to fix it?

  • Use Highlight and HighlightText for active or selected. That is the pair the user's own theme uses for selection, so it will always be distinguishable.
  • Never signal an error with a red border alone. Every field border resolves to the same system color, so the invalid one looks exactly like the four valid ones above it. Add an icon and a message wired up with aria-describedby, which is what WCAG 1.4.1 asks for regardless.
Example: Selected state for tab element
CSS

@media (forced-colors: active) {
    [role="tab"][aria-selected='true'] {
        background-color: Highlight;
        color: HighlightText;
    }
}

Example: A form marks invalid fields with a red border and nothing else. In High Contrast Mode every field has an identical border, so a user submitting a failed registration form gets told something is wrong with no way to find out which field. The same failure appears for anyone with red-green color blindness (deuteranopia), which is why 1.4.1 exists.

Trick 7: Modals that stay separate

Nowadays modals are separated from the page by either a semi-transparent overlay or backdrop-filter: blur() which both thankfully survive the High Contrast Mode. However if you ever encounter a modal that does not follow these patterns, it might render looking like a window on top of the text in High Contrast Mode. While not a direct accesibility violation, it is a good UX pattern to make sure your dialogs are clearly separated from the background.

How to fix it?

Give the dialog a transparent border. It is a good idea to go a bit thicker on large surfaces. Two or three pixels reads more clearly on a full-screen dialog than one.

Example: A dialog with a real edge
CSS

@media (forced-colors: active) {
    dialog, [role="dialog"] {
        border: 3px solid transparent;
    }
}

Example: A checkout moves its address step into a native <dialog> and never styles ::backdrop. In High Contrast Mode the default backdrop does not provide enough contrast, so from the user perspective, the dialog's background clips the order summary mid-sentence with no edge between them. The user reads it as a broken page, clicks the summary behind it, and gets nothing back, because the page behind a modal is inert. If you can't afford to add a backdrop, a border could provide enough clarity for the user to improve their experience.

Trick 8: Keep it in one place

High Contrast Mode styles are actually pretty simple to write. Most of what is broken can be fixed globally, because most of it is the same handful of properties failing the same way. Put a small base layer in your global stylesheet, then reach for a component-level override only when a specific component genuinely needs one. Below you can find our base layer that coverrs 99% of problems with High Contrast Mode:

Example: A global High Contrast Mode base layer
CSS

@media (forced-colors: active) {
    :focus-visible {
        outline: 2px solid transparent;
        outline-offset: 2px;
    }

    button, [type='button'], [type='submit'] {
        border: 1px solid transparent !important;
    }

    svg {
        stroke: currentColor !important;
    }

    button svg, [type="button"] svg {
        fill: ButtonText !important;
    }

    a svg {
        fill: LinkText !important;
    }

    dialog, [role="dialog"] {
        border: 3px solid transparent !important;
    }

    [role="tab"][aria-selected='true'] {
        background-color: Highlight !important;
        color: HighlightText !important;
    }

    [type="radio"] {
        opacity: 1;
        position: relative;
        clip: unset;
    }

    hr {
        height: 0;
        border-top: 2px solid CanvasText;
    }
}

When to override, and when not to

forced-color-adjust: none tells the browser to keep your colors on an element. Reaching for it to "adjust" the system palette to your design is the anti-pattern. People choose High Contrast Mode because they need those specific color pairings to read at all. Overriding them hands back the problem they turned it on to solve.

There is a narrow case where it is correct though. That is when the color is the information. A product swatch for a red t-shirt has to stay red, or the customer cannot pick a color. A status indicator using green, amber and red conveys nothing once all three become CanvasText.

Example: A product color swatch
CSS

@media (forced-colors: active) {
    .color-swatch {
        forced-color-adjust: none;
        border: 1px solid CanvasText;
    }
}

The border keeps the swatch visible against a matching background. Read the spec

One thing to remember is to not invert colors to make a component stand out, for example by swapping background and text against the user's chosen palette. It defeats the point of your users choosing their own colors.

How to test it

You can test the High Contrast Mode in the Chromium browser but in most cases, it will not be enough to cover the full testing which should happen on a native Windows environment.

DevTools cover the base tests in the coding phase. In Chrome or Edge open the command menu with Cmd/Ctrl + Shift + P, type Rendering, and switch on Emulate CSS media feature forced-colors. It is quick but applies only one default palette and does not simulate custom user themes.

Windows is the source of truth. left Alt + left Shift + Print Screen toggles contrast themes. You can also go to Settings → Accessibility → Contrast themes.

Contrast themes in Windows 11 Settings. A theme preview row shows four built-in themes, Aquatic, Desert, Dusk and Night sky, above a picker set to None with Apply, Edit and Delete buttons beside it.
Windows 11 Settings, Accessibility, Contrast themes. Four built-in themes, and the Edit button rewrites any of them color by color, which is why testing against a single palette proves very little. Used with permission from Microsoft.
  • Test a dark theme and a light theme. A component that survives Night sky can still vanish in Desert. The underlying fault is usually a missing boundary rather than a wrong color, and which theme exposes it is a matter of thorough testing.
  • Test with keyboard and tab through the whole flow. A focus ring that no longer exists is invisible to anyone using a mouse, including the person doing the testing.

One caveat for Mac-based teams. The macOS Increase Contrast setting is a different feature. It thickens borders and darkens text, but it does not force a palette and it does not trigger forced-colors: active. Testing on a Mac tells you nothing about any of this. Use a Windows VM, a spare laptop, or a hosted service like Assistiv Labs.

Key takeaways

  • Size the potential damage from your own Windows traffic. Microsoft puts High Contrast usage at about 4% of Windows users, and WebAIM's survey puts it near half of low vision users. Multiply by your Windows share and you got the amount of potentially frustrated clients who didn't convert.
  • No WCAG criterion names High Contrast Mode, but EN 301 549 clause 11.7 does. Because of this WCAG 1.4.1, WCAG 1.4.11 and WCAG 2.4.7 may fail on High Contrast Mode the same way they may fail on a standard environment.
  • Learn the property list. Color properties get swapped, box-shadow and text-shadow are forced to none, gradients disappear, and layout stays the same.
  • A transparent border is the cheapest fix you will ever ship. Your users will be thankful for the system-colored outline way more than you think.
  • Use outline for focus. box-shadow is forced to none, so a focus ring based on that property simply does not exist for these users.
  • Pair system colors, never mix them. CanvasText on Canvas, ButtonText on ButtonFace. Crossing the pairs produces way more problems than necessary.
  • Reach for forced-color-adjust: none only when color is the information. Swatches and status indicators are the exception. Apply it to the element itself to avoid any unintentional side-effects.

Sources:

Windows is a trademark of the Microsoft group of companies. All other trademarks are the property of their respective owners. This article is independent and is neither affiliated with, nor authorized, sponsored, or approved by, Microsoft Corporation.

Related categories:

Unlock your full potential

You have a product to build. You don't have time to become a WCAG expert. Let us handle the technicalities so your team can get back to what they do best.