AccessibilitySPEC-EVOLVINGPLATFORM-SPECIFICBROWSER-SPECIFIC

Contrast, Colour and Motion

Perceivability is measurable and it is a user preference. Contrast has a computed value, colour must never be the only carrier of meaning, and motion, contrast and target size are all things the operating system already knows about the person using your page.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Can this interface be perceived by someone with low vision, colour vision deficiency, a vestibular disorder or an imprecise pointer?

The user intent

Someone wants to read the text, tell the states apart, find the focused element and use the interface without being made nauseous or having to hit a nine-pixel target.

The obvious build

The design was signed off by a designer on a calibrated monitor, so it looks good. Colour is used consistently, and the animations make the product feel polished.

Why it breaks

Signed off means legible to that person, on that display, at that brightness, in that room. Low vision, a dimmed phone in sunlight, a cheap laptop panel and an ageing eye are all different tests.

How it breaks in a real browser
  • Signed off means legible to that person, on that display, at that brightness, in that room. Low vision, a dimmed phone in sunlight, a cheap laptop panel and an ageing eye are all different tests.
  • Roughly one in twelve men has a colour vision deficiency. Red-for-error and green-for-success are, for them, two greys — and a chart with six series is one grey.
  • Placeholder text, disabled states and secondary labels are the three places contrast is routinely lost, because "de-emphasised" was implemented as "lower the opacity until it looks right".
  • Large or parallax motion triggers nausea, dizziness and migraine for people with vestibular disorders. The operating system has a setting for this and most sites never read it.
  • Windows high-contrast mode replaces your palette wholesale. Focus indicators drawn with box-shadow, state shown with background-image, and icons drawn as CSS backgrounds all disappear, silently.
  • A 16-pixel icon button with four pixels of padding is a target that a tremor, a moving bus or a thumb cannot hit reliably — and a mis-hit next to a destructive action is not a minor inconvenience.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Contrast is a computed number, not a judgement: a ratio derived from the relative luminance of the foreground and the background colour. It can be calculated in CI, in a linter, and in devtools — which makes it one of the very few accessibility properties that is genuinely machine-decidable.
  • The thresholds are published by WCAG, differ by text size and by what the element is (body text, large text, user-interface component boundaries, graphical objects), and are versioned. WCAG 2.x defines one ratio formula; later work — the APCA research effort among it — proposes a perceptually different model, and which numbers apply depends entirely on the version you are conforming to. Look them up in that version rather than memorising a figure (Design Tokens).
  • Contrast applies to more than text. Focus indicators, form field boundaries, icon-only controls, chart lines and state indicators all carry information, and information the user cannot see is information you did not convey.
  • Colour vision deficiency removes distinctions rather than colour. The interface still has colours; some pairs of them are now the same. Redundant encoding — an icon, a label, a pattern, a position — restores the distinction for everyone.
  • The browser exposes user preferences as media features: prefers-reduced-motion, prefers-contrast, prefers-color-scheme, forced-colors, and prefers-reduced-transparency where implemented. These come from the operating system, so the user has already told you (Media Queries Beyond Width).
  • forced-colors: active means the browser has substituted a system palette. Most of your colour declarations are ignored; the CSS system colour keywords (CanvasText, Highlight, LinkText, ButtonText) are what remains, and forced-color-adjust lets you exempt content like a colour picker where the real colour is the point.
  • Target size is spacing as much as dimensions: a small control with generous spacing around it is far more reliably hit than a slightly larger one wedged between two neighbours. The published minimums are, again, versioned and defined by WCAG rather than by any one platform (Pointer Events).

What this makes the browser do

And which of it is avoidable.

  • Reduced-motion handling in CSS costs nothing — it is a media query the style engine already evaluates. Doing it in JavaScript with matchMedia costs one listener.
  • Animating transform and opacity is compositor work; animating colours, shadows and layout properties is main-thread work per frame, and a page full of "polish" animations is a frame-budget problem as well as a motion problem (Cheap and Expensive Animation).
  • In forced-colours mode the browser is doing extra work substituting your palette. Fighting it with !important or with images of text costs you correctness and gains nothing.
  • Avoidable: animating anything at all on a preference-reduced session. Skipping the animation is not just polite, it removes the frames entirely (The Frame Budget).

Contrast is computed, and the number is not yours to remember

SPEC-EVOLVINGDeliberately no numbers in this table. The thresholds for each row are defined by the WCAG version being conformed to, differ between small text, large text and non-text elements, and have changed between revisions — the durable knowledge is which pairs to check, not what the current figures are.

Contrast is unusual among accessibility properties: it is a function of two colours with a defined output. That makes it enforceable in a build rather than discoverable in an audit, which is a much better place for it to live.

What you must not do is write the required ratio into your code or your head as though it were permanent. The requirements are published by WCAG, they differ by text size and by what kind of element it is, and the underlying formula is itself under revision — the perceptual model proposed in current research does not rank the same colour pairs the same way. Encode "check against the version we conform to", not "4-point-something".

The second half of the problem is that a ratio is computed against a background, and real elements sit on many. The table below is the set of pairs a component actually has to survive.

WhatAgainst whatWhy it is missedWhere to check it
Body textIts own surface, in light and dark modeChecked in one theme onlyToken pair enumeration in CI (Design Tokens)
Secondary and disabled textThe same surface"It is meant to be quiet" treated as exemptSame, with the muted tokens included
Placeholder textThe input backgroundTreated as decoration rather than as contentManually, plus removing it as a label entirely
Focus indicatorEvery background it can land on: hover, selected row, coloured button, dark mode, forced coloursOnly checked on the default page backgroundForce :focus-visible in devtools across states
Icon-only controlsTheir button surfaceIcons are assumed to be exempt because they are not textNon-text contrast requirements in the current WCAG version
Chart series and status dotsThe plot background and each otherDistinguishability between series is a separate question from contrast with the backgroundColour-vision simulation plus redundant encoding
Text over an image or gradientThe worst pixel underneath itAutomated checkers decline to answer hereA scrim, a solid plate, or text outside the image

Colour is never the only channel

A colour-coded interface is one where a subset of users has been silently handed a monochrome version. This is not a rare edge: colour vision deficiencies affect a substantial minority of users, and the affected pairs — red and green above all — are exactly the pairs interfaces use for failure and success.

The fix is redundant encoding, and it is cheap. Add a second channel that carries the same information: a word, an icon with a distinct silhouette, a pattern, a position, an underline. Everybody benefits, because the second channel also survives sunlight, bad monitors, greyscale printing and a glance from across the room.

A status column in a table
Colour carries the meaning
<td><span class="dot dot--red"></span></td>
<td><span class="dot dot--green"></span></td>

<style>
  .dot--red   { background: #d33; }
  .dot--green { background: #2a2; }
</style>

<!-- Two grey dots for a red-green colour vision deficiency.
     Nothing at all in the accessibility tree.
     Nothing at all when the page is printed in greyscale. -->
Colour is the fourth channel, not the first
<td>
  <span class="status status--failed">
    <svg aria-hidden="true" focusable="false"><use href="#x-circle" /></svg>
    Failed
  </span>
</td>
<td>
  <span class="status status--passed">
    <svg aria-hidden="true" focusable="false"><use href="#check-circle" /></svg>
    Passed
  </span>
</td>

<style>
  /* Colour reinforces a distinction the word and the shape
     already made. Removing it degrades the design, not the meaning. */
  .status--failed { color: var(--fg-danger); }
  .status--passed { color: var(--fg-success); }
</style>

The word carries the state for a screen reader, for a greyscale print and for anyone with a colour vision deficiency; the icon silhouette carries it at a glance and at distance; the colour makes it fast to scan for everyone else. Three channels, one of which is optional — as against one channel that a substantial minority of users does not receive at all.

The system already knows: preferences as input

Motion sensitivity, contrast preference, colour scheme and transparency are settings the user configured at the operating system level, often years ago, because they needed to. The browser forwards them. Reading them is the difference between an interface that adapts and one that overrides a medical accommodation for the sake of a hero animation.

Two mistakes recur. The first is handling reduced motion in CSS only, leaving JavaScript-driven scroll effects, canvas transitions and autoplaying video untouched. The second is treating "reduce" as "remove", so a state change that used to be revealed by an animation now never appears at all — the user is left with no indication that anything happened.

What disappears, and where
TriggerSymptomCauseResponse
Windows high-contrast mode is onThe focus ring is invisibleThe indicator was drawn with box-shadow, which forced-colours discardsUse outline, and set outline-color to a system keyword inside @media (forced-colors: active).
Windows high-contrast mode is onIcon buttons are blank squaresIcons were CSS background-images, which are not painted in forced coloursUse inline SVG with currentColor, or real text.
Reduced motion is setA panel never appearsA global animation: none rule removed the reveal animation that also set the final stateReduce specific transitions; never blanket-disable animation that carries state.
Reduced motion is setThe page still parallaxes and the carousel still autoplaysOnly CSS was gated; the JavaScript effects ignore the preferencematchMedia in the effect code, and pause autoplaying media by default (Images, Video and the Elements That Own Their Layout).
The user zooms text to 200%Buttons clip their labels; a horizontal scrollbar appearsFixed pixel heights and widths with overflow: hiddenSize with content-relative units and let containers grow (Responsive Typography).
A dense table on a phoneUsers hit Delete instead of EditAdjacent icon targets with almost no spacingIncrease the hit area and the spacing between adjacent actions; separate destructive actions from routine ones.
Reduced motion, forced colours, and the JavaScript half
1/* Reduce, do not delete. The change must still be perceivable —
2 it just must not fly across the viewport to get there. */
3.panel {
4 transition: transform 240ms ease, opacity 240ms ease;
5}
6
7@media (prefers-reduced-motion: reduce) {
8 .panel {
9 /* keep the state change, drop the travel */
10 transition: opacity 120ms linear;
11 transform: none;
12 }
13 /* Blanket rules like *:not(:has(...)) { animation: none !important }
14 also remove reveal animations, leaving content permanently hidden.
15 Target the motion you actually authored. */
16}
17
18/* The user asked for more contrast: honour it rather than
19 preserving the subtle palette. */
20@media (prefers-contrast: more) {
21 :root {
22 --fg-muted: var(--fg-default);
23 --border-subtle: var(--border-strong);
24 }
25}
26
27/* The palette has been replaced by the system. Anything drawn with
28 box-shadow, background-image or a custom property is now gone. */
29@media (forced-colors: active) {
30 .card { border: 1px solid CanvasText; }
31 :focus-visible { outline: 3px solid Highlight; outline-offset: 2px; }
32 .status-dot { forced-color-adjust: none; } /* the colour IS the data */
33}

The JavaScript half is not optional: matchMedia('(prefers-reduced-motion: reduce)') should gate scroll-driven effects, scrollIntoView({ behavior: 'smooth' }), canvas animation loops and video autoplay, none of which any CSS rule can reach.

How to build it

Most important first.

  • Check contrast at design time and enforce it at build time. Contrast between token pairs is computable, so a design system can assert its own palette combinations rather than discovering failures in an audit (Design Tokens).
  • Never convey meaning with colour alone. Pair every colour signal with a second channel: an icon, a word, a shape, a pattern, a position, an underline.
  • De-emphasise with size, weight, position or spacing rather than by fading toward the background. Opacity-based dimming is the most common way contrast is lost.
  • Honour prefers-reduced-motion by reducing, not removing: keep a state change visible so the user knows something happened. A cross-fade or an instant change usually replaces a slide or a scale safely; large translations, parallax and autoplay are what the preference is about.
  • Read the preference in JavaScript too. A CSS-only implementation leaves your scroll-driven animations, canvas transitions and autoplaying video untouched (Images, Video and the Elements That Own Their Layout).
  • Test in forced-colours mode and fix what disappears: replace box-shadow focus rings with outline, replace CSS-background icons with real SVG or text, and use system colour keywords where the palette is discarded.
  • Size targets generously and space them apart, especially in toolbars, table row actions and anything adjacent to a destructive control (Responsive Typography).
  • Support zoom and reflow: text scaling to 200% and page zoom well beyond it should not clip content or produce horizontal scrolling. Fixed pixel heights and overflow: hidden are the usual culprits (Fluid Layout First).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • This lesson is the perceivability half of the module. Semantics and keyboard operability do nothing for a person who cannot see the difference between the focused element and the rest of the page.
  • The focus indicator is subject to contrast requirements like any other meaningful visual, against every background it can appear over (Focus Management).
  • Motion preferences are an access requirement, not a taste setting: for a person with a vestibular disorder, a parallax hero is a physical symptom.
  • Redundant encoding helps far more people than it was designed for — colour-blind users, users in sunlight, users on a bad monitor, users glancing at a dashboard from three metres away.
  • Automated contrast checks are the most reliable automated accessibility checks that exist, and they still cannot evaluate text over images, over gradients, or in states the crawler never triggered (Accessibility Testing).

What can go wrong

Failure modes
  • Contrast checked for the default state only, and lost on hover, on a selected row, in dark mode, or over a photograph where the ratio changes per pixel.
  • Disabled controls with contrast so low they are unreadable — often defended as "they are disabled anyway", which assumes the user does not need to know what the control was.
  • Placeholder used as the label: it starts at low contrast and disappears the moment the user types, taking the field's meaning with it (Input Types, Inputmode and Autocomplete).
  • Dark mode implemented with a CSS filter: invert(), which inverts photographs, logos and charts along with the text and destroys contrast relationships wholesale.
  • A reduced-motion media query that sets animation: none globally, breaking components that used an animation to reveal content — the content is now permanently invisible. Reduce the motion; do not delete the state change.
  • The mitigation failing: a contrast linter over design tokens that passes, while the actual component composes a token over a gradient the linter never saw.
  • Focus indicators drawn with box-shadow, which forced-colours mode discards — the users most dependent on a visible focus ring are the ones who lose it.
Security
  • Low contrast is a deception surface. Text made near-invisible against its background hides disclosures and consent copy in plain sight, and legal review rarely checks computed ratios.
  • Motion and colour are used in clickjacking and overlay attacks to make an element appear inactive or absent while it is still receiving input (Clickjacking and Framing).
  • Respecting prefers-* media features exposes a little more about the user's system, which contributes to fingerprinting — a real but small cost against a real and large accessibility benefit (Session Replay and the Privacy It Costs).
Misreads
  • "We hit the contrast number, so the design is accessible." Contrast is one of several perceivability requirements, and the number applies per state, per background, per mode.
  • "Reduced motion means remove all animation." It means reduce large, position-changing motion. A user still needs to perceive that something changed, and an instantaneous swap can be more disorienting than a short fade.
  • "Nobody uses high-contrast mode." It is a default-available operating system feature used by a substantial number of low-vision users, and it is the mode where hand-drawn focus indicators disappear.
  • "Colour blindness means seeing in greyscale." Overwhelmingly it means specific pairs of colours becoming indistinguishable — most often red and green — while the rest of the interface looks normal.
  • "Contrast thresholds are a fixed number I can memorise." They are published, versioned, and differ by text size and element type. The formula itself is under active revision.

Measuring it, and what changes in the field

How you would see this
  • DevTools contrast tooling: Chrome's colour picker shows the computed ratio against the resolved background and flags failures; Firefox's Accessibility panel can audit the whole page for contrast. Both give up over gradients and images, which is where the real failures hide.
  • Emulate the preferences rather than changing your OS settings: Chrome and Firefox both expose prefers-reduced-motion, prefers-contrast, forced-colors and colour-vision deficiency simulations in the rendering tools.
  • Compute contrast over the design token palette in CI. Every valid foreground/background pair is enumerable, so the failure can be a build error instead of an audit finding (Design Systems).
  • Zoom the browser to 200% and to 400% and complete a flow. Clipping and horizontal scrolling show up immediately.
  • Turn on the operating system's high-contrast mode and look at what vanished.
Slow device, slow network, large data, old tab
  • Outdoors, on a phone, at low brightness, effective contrast collapses — the design that was comfortable indoors is unreadable in the sun. This is the most common low-vision experience and almost nobody tests it.
  • On OLED and on cheap LCD panels the same colours render differently, and dark mode behaves differently again: pure white on pure black produces halation that makes text harder to read for some people, not easier (The Viewport and Device Pixels).
  • On touch, target size dominates every other consideration, and it is worst exactly where actions are densest — table rows, toolbars, chips.
  • On a slow device, animations drop frames, and a janky animation is more disorienting than a snappy one. Reduced motion helps performance and comfort at once (Interaction Responsiveness).
  • In a translated interface, text grows: German and Finnish routinely need noticeably more width than English, and fixed-height containers clip rather than reflow (Internationalization).
What this costs
  • Contrast requirements constrain brand palettes, and the constraint is real: some brand colours simply cannot be used for small text on white. The resolution is a palette with accessible variants, decided once, not a per-component argument.
  • Redundant encoding costs visual density — an icon next to every status label is more ink on the screen. It is also legible from further away and on worse displays.
  • Honouring reduced motion means maintaining two versions of every meaningful transition, which is real work and mostly consists of deleting things.
  • Larger targets take space, and space is exactly what a dense data interface does not have. The trade is usually resolved with spacing rather than size.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • SPEC-EVOLVINGWCAG publishes the contrast and target-size requirements, they differ by text size and element type, and they are versioned — the ratio formula in WCAG 2.x and the perceptual model proposed in later work do not produce the same verdicts. Read the thresholds from the version you are conforming to; a number written into a lesson or a component is wrong the moment the specification moves.
  • PLATFORM-SPECIFICThe preferences come from the operating system and do not map cleanly onto each other: Windows high-contrast drives forced-colors: active and substitutes the palette, macOS "Increase contrast" surfaces as prefers-contrast without replacing colours, and reduced-motion settings live in different places on Windows, macOS, iOS and Android with different scopes. A page can respond correctly to one and ignore another.
  • BROWSER-SPECIFICDevtools contrast checkers compute against the resolved background of a single element and decline to answer over gradients, images and transparency — Chrome flags this explicitly, Firefox audits the page but with the same limitation, and Safari expects you to use external tooling. Support for prefers-reduced-transparency and for forced-color-adjust also differs between engines.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Testing & Reliability Engineering — contrast and preference handling are the parts of accessibility that automate well: computing ratios over a token palette in CI, and running visual regression suites under emulated prefers-reduced-motion and forced-colors so a disappearing focus ring fails a build.