Respecting Reduced Motion and Forced Colors
Make extension popups, panels and injected UI respect prefers-reduced-motion, forced-colors (Windows High Contrast), prefers-contrast and color-scheme: CSS media queries, system colours, focus visibility and testing in DevTools.
Table of Contents
- The user preference media features
- Step-by-step: honouring display preferences
- 1. Make motion opt-in by default
- 2. Replace essential motion with non-vestibular alternatives
- 3. Read the preference in JavaScript for scripted motion
- 4. Survive forced colours
- 5. Keep focus visible everywhere
- 6. Apply the same rules to injected UI
- 7. Declare your colour schemes
- 8. Emulate preferences in DevTools
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Related
A user with a vestibular disorder opens the extension’s popup and gets a sliding, bouncing panel that makes them nauseous; another user on Windows High Contrast mode sees a toolbar of invisible icon buttons, because the extension drew them with background images and custom colours the system overrode. Both users set an operating-system preference that the browser passes to every page as a CSS media feature — prefers-reduced-motion and forced-colors — and the extension ignored them. Extension UI is small, so honouring these preferences is cheap: a few media queries, system colour keywords and a habit of testing with DevTools emulation. This guide covers the popup, options, side panel and UI injected into pages. It belongs to internationalization and accessibility.
The user preference media features
Browsers expose several OS-level display preferences to CSS and JavaScript. prefers-reduced-motion: reduce means the user asked for less motion — remove or shorten non-essential animation. forced-colors: active means the browser is enforcing a limited user-chosen palette (Windows High Contrast / Contrast Themes): author colours, backgrounds and shadows are replaced by system colours, so meaning carried only by colour or background images disappears. prefers-contrast: more is a softer request for higher contrast. prefers-color-scheme: dark asks for a dark theme. All work in extension pages exactly as on the web, and in content-script-injected UI because it lives in the page’s document. Each can be read in JavaScript with matchMedia.
Step-by-step: honouring display preferences
1. Make motion opt-in by default
1/* popup.css */
2.panel { opacity: 1; transform: none; }
3
4@media (prefers-reduced-motion: no-preference) {
5 .panel { transition: transform 200ms ease, opacity 200ms ease; }
6 .panel[data-entering] { transform: translateY(8px); opacity: 0; }
7}
Execution context: extension page CSS. Writing the animated version inside no-preference means the default is static, so anyone whose browser reports reduce — or an older engine that doesn’t report anything — gets no motion. That is safer than adding animation globally and trying to remove it later.
2. Replace essential motion with non-vestibular alternatives
1@media (prefers-reduced-motion: reduce) {
2 .spinner { animation: none; }
3 .spinner::after { content: "…"; } /* still shows that work is happening */
4 .toast { transition: opacity 150ms linear; } /* a fade is acceptable; a slide is not */
5}
Execution context: extension page CSS. “Reduced” does not mean “no feedback”: progress must still be visible. Fades and colour changes are generally fine; large movements, zooms, parallax and auto-playing carousels are what cause problems. Keep any remaining transition short.
3. Read the preference in JavaScript for scripted motion
1const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)");
2
3function scrollToItem(el) {
4 el.scrollIntoView({ behavior: reduceMotion.matches ? "auto" : "smooth", block: "nearest" });
5}
6reduceMotion.addEventListener("change", () => document.body.toggleAttribute("data-reduce-motion", reduceMotion.matches));
Execution context: extension pages and content scripts. Smooth scrolling, element.animate(), canvas animations and libraries such as Framer Motion all need the JavaScript check. Listening for change updates an open side panel when the user toggles the OS setting. Many animation libraries accept a reduced-motion flag; pass this value through.
4. Survive forced colours
1.icon-button {
2 background: var(--surface);
3 border: 1px solid transparent; /* becomes visible in forced colours */
4 color: var(--text);
5}
6.icon-button svg { fill: currentColor; } /* follows the system text colour */
7
8@media (forced-colors: active) {
9 .icon-button { border-color: ButtonText; }
10 .icon-button:focus-visible { outline: 2px solid Highlight; }
11 .badge { forced-color-adjust: none; background: Highlight; color: HighlightText; }
12}
Execution context: extension page CSS. In forced colours mode the browser replaces your colours with system ones and removes backgrounds and box shadows, so controls distinguished only by background colour disappear. A transparent border costs nothing normally and becomes a visible outline under forced colours. Inline SVG icons with fill: currentColor follow the system text colour; icons drawn as CSS background images vanish. Use system colour keywords (ButtonText, Highlight, CanvasText, LinkText) when you must set colours explicitly, and use forced-color-adjust: none sparingly, only on elements whose colour you deliberately map to system colours.
5. Keep focus visible everywhere
1:focus-visible { outline: 2px solid var(--focus, Highlight); outline-offset: 2px; }
2@media (forced-colors: active) { :focus-visible { outline-color: Highlight; } }
Execution context: extension page CSS. Many designs replace outlines with a box-shadow ring, which disappears under forced colours. Using outline keeps the focus indicator in every mode. See making popups and options keyboard-navigable.
6. Apply the same rules to injected UI
1// content script: inject into a shadow root with its own stylesheet
2const sheet = new CSSStyleSheet();
3sheet.replaceSync(`
4 @media (prefers-reduced-motion: no-preference) { .fab { transition: transform .15s; } }
5 @media (forced-colors: active) { .fab { border: 2px solid ButtonText; } }
6 .fab svg { fill: currentColor; }
7`);
8shadowRoot.adoptedStyleSheets = [sheet];
Execution context: a content script. Media queries inside a shadow root’s styles evaluate against the user’s preferences just as page CSS does. Injected overlays are often the most animated part of an extension, so they matter most. See building a floating action button on web pages.
7. Declare your colour schemes
1:root { color-scheme: light dark; }
2@media (prefers-color-scheme: dark) { :root { --surface: #1e1e24; --text: #e8e8ee; } }
Execution context: extension page CSS. color-scheme lets the browser render form controls and scrollbars to match, and is respected in forced colours mode. See theming the options page for dark mode.
8. Emulate preferences in DevTools
Open DevTools for the popup or options page, then the Rendering panel (Command menu → “Show Rendering”), and set “Emulate CSS media feature” for prefers-reduced-motion, forced-colors, prefers-contrast and prefers-color-scheme. Check every screen in each mode. On Windows, also test with a real Contrast Theme, since emulation does not reproduce every system colour.
Common mistakes
- Animation by default, removed under
reduce. Make motion opt-in instead. - Icons as CSS background images. They disappear in forced colours.
- Box-shadow focus rings. Invisible in forced colours; use outline.
- Blanket
forced-color-adjust: none. Defeats the user’s choice. - Forgetting injected UI. Overlays on pages need the same rules.
Cross-browser variation
- Chrome / Edge: support all four media features;
forced-colorsis active with Windows Contrast Themes. - Firefox: supports all four; Firefox has its own colour override setting that also activates
forced-colors. - Safari: supports
prefers-reduced-motion,prefers-contrastandprefers-color-scheme;forced-colorssupport depends on version — honouring it costs nothing where unsupported.
Verification
- Emulate
prefers-reduced-motion: reduceand confirm no element slides, zooms or bounces. - Emulate
forced-colors: activeand confirm every button, icon and focus ring remains visible. - Toggle the OS setting with a side panel open and confirm it updates live.
- Inject the in-page UI on a dark site and a light site under each mode.
FAQ
Is a fade acceptable under reduced motion?
Generally yes. Short opacity changes do not trigger vestibular symptoms the way movement does.
Do I need to support forced colours if I already have a dark mode?
Yes. They are different: forced colours replaces your palette entirely with the user’s chosen system colours.
Does the toolbar icon need high-contrast variants?
The browser renders the toolbar; provide icons with clear shapes that read at small sizes on both light and dark toolbars. Use action.setIcon with alternate images if your icon disappears against a dark theme.
How do I test without Windows?
DevTools emulation covers most cases. For confidence, test once on Windows with a Contrast Theme before release.
Related
- Making popups and options keyboard-navigable — focus and keyboard.
- Managing focus in popups and dialogs — where focus goes.
- Theming the options page for dark mode — colour schemes.
- Internationalization and accessibility — the parent topic.