High Contrast Mode (HCM) Media Queries

Firefox supports both the prefers-contrast and forced-colors media queries. These queries detect a user’s display settings (in the browser and/or in their OS), and allow authors to adjust content styling accordingly. These queries are not interchangeable, and we should be intentional about using one or the other (or both!) when designing for accessibility.

What activates these queries?

Forced Colors

The forced-colors media query has two values: active and none. The resolved value of this media query is determined by the browser based on user settings (OS and Firefox).

  • The none state indicates documents can use any colors to render content.

  • The active state indicates a user has OS or browser settings that limit the color palette available to the User Agent.

In Firefox’s default configuration, the active state is triggered when a user has enabled Windows High Contrast Mode or Firefox High Contrast Mode (FF HCM). Both of these forced-colors modes offer users a limited amount of color customisation. In Windows, users can override seven colors, and with Firefox High Contrast Mode users can override four. MacOS’s “Increase Contrast” setting does not trigger forced-colors: active because it does not limit the color palette available to UA’s.

NOTE: MacOS’s “Increase Contrast” setting will trigger forced-colors: active (in non-chrome web content) if it is enabled in combination with FF HCM’s “Only with High Contrast Themes” option. This is also true on Linux. Because FF HCM defaults to “Never” on non-Windows platforms, enabling “Increase Contrast” or Linux High Contrast Themes alone will not trigger forced-colors: active. Similarly, if a user disables FF HCM while Win HCM is enabled, forced-colors (in content) will evaluate to none.

CSS System Colors

Colors that are overridden by the forced-colors modes list above are inherited into the browser’s CSS System Colors. Designers must use these colors exclusively in their designs when designing for forced-colors modes. CSS System Colors are intended to be used in pairs: for example the ButtonFace and ButtonText colors should be used together to create styling for interactive controls. The full pairings are listed in the system color pairings section of the CSS spec. Mozilla has additional design guidance for using CSS System Colors in our HCM Design Guide. You can also check out our other docs to learn more about color overriding.

Prefers Contrast

The prefers-contrast media query has four values: more, less, custom, and no-preference.

  • The more state is triggered when a user has macOS’s “Increase Contrast” setting enabled, or when a forced-colors mode is active and the ratio between a user’s chosen foreground and background is higher than 7:1. This check is done in our style system.

  • The less state is triggered when a forced-colors mode is active and the foreground/background ratio is less than 4.5.

  • When a forced-colors mode is enabled with a ratio between these two values, we return custom.

  • Otherwise, if no forced-colors mode is active, and macOS’s “Increase Contrast” is off, the media query resolves to no-preference.

So what do I use?

Generally, this boils down to which operating systems you want your styling to apply to. In the past, we’ve often written code to target all platforms within prefers-contrast blocks because the kinds of changes we make to target forced-colors mode also benefit “Increase Contrast” users on macOS; they allow system colors to propagate to the UA. The palette that macOS supplies when “Increase Contrast” is enabled is also inherited to CSS System Colors, and may offer improvements over the standard palette for a design. It is still possible that changes made for forced-colors modes benefit “Increase Contrast” users, however we strongly encourage developers to consider whether diversifying our designs for these modes provides better UX.

On macOS, we aren’t limited to the OS palette. Instead of modifying a design to use fewer colors, we can simply increase the contrast ratio of the existing colors to 7:1 to satisfy the enhanced contrast criterion of Web Content Accessibility Guidelines (WCAG) for better accessibility. Consider the following designs.

Standard UX on macOS:

Firefox view page. The navigation tabs are rendered to match the custom browser theme -- yellow on dark grey, with the active tab rendered light Firefox blue on blue-grey. Page content is rendered yellow on dark grey. Interactive components are a slightly lighter grey, difficult to distinguish against the page background.

UX with increased contrast of existing colors on macOS:

Firefox view page. The navigation tabs are rendered yellow on darker grey, with the active tab rendered white on darker blue. Page content is rendered yellow on dark grey with white borders around interactive components.

Changes present in this example belong in a prefers-contrast: more block. This is assuming these changes are appropriately overridden in the cascade for forced-colors: active users. If they are not overridden, an additional query like @media (-moz-platform: macos) or .. and (not (forced-colors)) should be combined with prefers-contrast: more to ensure these changes only apply to macOS. For users who prefer less contrast, we can supply colors that lower the contrast ratios in the existing design and encase those changes in a prefers-contrast: less block.

UX with limited color palette in FF HCM on macOS:

Firefox view page. The navigation tabs are rendered white on dark grey, with the active tab rendered black on white. Page content is rendered white on black with borders around interactive components.

In forced-colors modes, we require that designs reduce their palettes for compatibility. To accomplish this, we use CSS System Colors instead of the original design colors. This allows the overridden palette to “show through”, since colors from the various forced-colors modes are inherited into CSS System Colors. It is possible that using CSS System Colors also increases contrast for users on macOS, but this more drastically changes the design at hand, and can detract from the overall user experience for users who are simply looking for more contrast. Designers should consider the additional guidelines on border use, removing gradients, etc. in our HCM Design Guide.

NOTE: Firefox’s chrome style sheets are subject to different rules than web-author style sheets.

Firefox’s chrome style sheets (including those in our about: pages) are not prevented from using colors that aren’t system colors, even if a forced colors mode is enabled. In webpages, however, Firefox forces CSS System Colors regardless of the author’s original specification. This is a safety mechanism to ensure pages render appropriately even if no @media (prefers-contrast) or @media (forced-colors) styling was specified. To avoid this overriding, web authors can use the forced-color-adjust: none; CSS property.

Writing Maintainable Frontend Code

Where possible, we prefer color overriding for prefers-contrast and forced-colors take place in one block at the root level rather than in many blocks on an element-by-element basis. We encourage developers to use design systems tokens with semantic naming when overriding. Consider the following.

:root {
  --page-background: #cccccc;
  --page-text-color: #000000;

  @media (forced-colors) {
    --page-background: Canvas;
    --page-text-color: CanvasText;
  }
}

body {
  color: var(--page-text-color);
  background-color: var(--page-background);
}

Here, we’ve defined one set of color variables on :root and subsequently overridden them in a forced-colors block. We don’t need to re-write the styling for body within our media query because the vars it references will correctly adapt when forced-colors is active.

Though forced-colors overriding doesn’t happen in Firefox chrome style sheets, our style system does force all instances of transparent to the default color for a color attribute (usually CanvasText). This allows us to specify styles that appear only in forced-colors mode without an additional CSS keyword. It is also sometimes beneficial to define vars at the :root level with an initial value (eg. transparent, 0px, none) so the vars exist for HCM overriding later (see: --dialog-border-width).

:root {
  /* ... */
  --dialog-background: #ffffff;
  --dialog-color: #bbbbbb;
  --dialog-border-color: transparent;
  --dialog-border-width: 0px;

  @media (forced-colors) {
    /* ... */
    --dialog-background: Canvas;
    --dialog-color: CanvasText;
    /* No override for --dialog-borer-color, since it'll become
    * CanvasText when HCM is enabled, which matches the HCM spec. */
    --dialog-border-width: 1px;
  }
}

.dialog {
  color: var(--dialog-color);
  background-color: var(--dialog-background);
  border: var(--dialog-borer-width) solid var(--dialog-border-color);
}

/* ... */

In general, it is best to do overriding at the :root level, even if additional variables are required. It is not recommended to do overriding on a class-by-class or element-by-element basis, like below:

:root {
  /* ... */
  --button-background: #ffffff;
  --button-color: #bbbbbb;

  @media (forced-colors) {
    /* ... */
    --button-background: ButtonFace;
    --button-color: ButtonText;
    --button-border-color: var(--button-color);
  }
}

@media (forced-colors) {
  /* BAD: These rules are generic and should be outside of a @media block */
  .destroyButton {
    color: var(--button-color);
    background-color: var(--button-background);
    border: 1px solid var(--button-border-color);
  }
}

@media (not (forced-colors)) {
  /* BAD: These rules are generic and should be outside of a @media block */
  .destroyButton {
    color: var(--light-grey-20);
    background-color: var(--red-60);
  }
}
/*...*/

Putting it all together!

Let’s walk through an example on this sample website.

The majority of our site styling is done via color overriding on the root block.

:root {
  /* General */
  --background: #202124;
  --color: #cccccc;
  --border-color: transparent;
  --border-size: 0;
  --table-background: #303134;
  /* Note */
  --note-background: gold;
  --note-color: var(--table-background);
  --note-opacity: .7;
  /* General Controls */
  --control-background: gray;
  --control-color: var(--color);
  --control-border-size: var(--border-size);
  --control-border-color: var(--border-color);
  /* Destructive Button */
  --destructive-button-background: tomato;
  --destructive-button-color: var(--table-background);
}

How might we adapt this website for users who prefer increased contrast? What about for users with forced colors?

Let’s start with users who prefer increased contrast. We might decide to make the background and foreground easier to read by making the foreground text lighter, increasing the contrast ratio. We might also remove the note’s opacity and darken its text color. We don’t need to remove the note’s golden background, since prefers-contrast doesn’t require a reduced color palette.

:root {
  /* ... */
  @media (prefers-contrast) and (not (forced-colors)) {
    /* General */
    --color: white;
    /* Note */
    --note-color: black;
    --note-opacity: 1;
    /* ... */
  }
}

To address these same issues in forced-colors mode, we should replace the colors with the semantically appropriate system color. Unlike our work in the prefers-contrast block above, we do need to modify the note’s background, since gold is not a system color. We might end up with something like this:

:root {
  /* ... */
  @media (forced-colors) {
    /* General */
    --background: Canvas;
    --color: CanvasText;
    --table-background: var(--background);
    /* Note */
    --note-background: var(--background);
    --note-color: var(--color);
    --note-opacity: 1;
    /* ... */
  }
}

After this change, you’ll notice our page background, table background, and note background all share the same color. This makes them difficult to differentiate. To address this, we can add a contrasting border and override the previously transparent --border-color variable and its corresponding --border-size. This var applies to content areas but not controls.

:root {
  /* ... */
  @media (forced-colors) {
    /* General */
    --background: Canvas;
    --color: CanvasText;
    --border-color: var(--color);
    --border-size: 1px;
    --table-background: var(--background);
    /* Note */
    --note-background: var(--background);
    --note-color: var(--color);
    --note-opacity: 1;
    /* ... */
  }
}

Next, let’s look at the controls this page uses for the web form. We’ve got an input, a checkbox, a submit button, and a clear button. In the prefers-contrast case, we should ensure the controls contrast from the background as much as possible. It’s possible to do this via color alone, but it can help to add borders for additional contrast. Here, again, we don’t need to get rid of the clear button’s tomato background, but we can update it to something brighter.

:root {
  /* ... */
  @media (prefers-contrast) and (not (forced-colors)) {
    /* ... */
    /* General Controls */
    --control-background: color-mix(in srgb, gray, lightgray);
    --control-color: black;
    --control-border-size: 1px;
    --control-border-color: gray;
    /* Destructive Button */
    --destructive-button-background: red;
    --destructive-button-color: white;
    /* ... */
  }
}

Finally, let’s style the page’s controls for forced-colors. Unlike in prefers-contrast, we can’t use color to differentiate between the submit button and the clear form button – both should inherit from our button CSS system colors.

:root {
  /* ... */
  @media (forced-colors) {
    /* General */
    --background: Canvas;
    --color: CanvasText;
    --border-color: var(--color);
    --border-size: 1px;
    --table-background: var(--background);
    /* Note */
    --note-background: var(--background);
    --note-color: var(--color);
    --note-opacity: 1;
    /* General Controls */
    --control-background: ButtonFace;
    --control-color: ButtonText;
    --control-border-color: ButtonText;
    /* Destructive Button */
    --destructive-button-background: var(--control-background);
    --destructive-button-color: var(--control-color);
  }
}

If we wanted to make the submit button stand out as a primary button, we could invert the styling on that button in particular. Something like:

:root {
  /* ... */
  @media (forced-colors) {
    /* General */
    --background: Canvas;
    --color: CanvasText;
    --border-color: var(--color);
    --border-size: 1px;
    --table-background: var(--background);
    /* Note */
    --note-background: var(--background);
    --note-color: var(--color);
    --note-opacity: 1;
    /* General Controls */
    --control-background: ButtonFace;
    --control-color: ButtonText;
    --control-border-color: ButtonText;
    /* Destructive Button */
    --destructive-button-background: var(--control-background);
    --destructive-button-color: var(--control-color);
  }

  #submit {
    background: var(--control-color);
    color: var(--control-background);
    border: var(--control-border-size) solid currentColor;
  }
}

Now we’ve got a site that functions in multiple HCM scenerios :) Visit the live site with HCM or Increase Contrast enabled to test it for yourself. A few takeaways:

  • prefers-contrast and forced-colors are not mutually exclusive. If you write two independent forced-colors and prefers-contrast media query blocks, they’ll both apply when FF HCM is enabled (assuming your foreground/background contrast ratio is high, which it is by default). Adding an and (not (forced-colors)) clause to your @media (prefers-contrast) declaration can help make the two blocks distinct if you’d like mac-centric styling in one and forced-colors styling in the other.

  • prefers-contrast requires specific, case-by-case overriding, whereas forced-colors is largely about inheretince. In the former, page regions that are differentiated with color should stay that way (albeit with more contrasting colors). In forced-colors all page regions that aren’t interactive should use Canvas/CanvasText and rely on borders for distinction. Once you’ve set --background: Canvas; at the root level, for ex. subsequent background vars should inherit from it.

  • Where possible, aim to do overriding at the :root level using CSS variables, this makes it easier to update code in the future.