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.
Can this interface be perceived by someone with low vision, colour vision deficiency, a vestibular disorder or an imprecise pointer?
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 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.
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.
- 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 withbackground-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.
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, andprefers-reduced-transparencywhere implemented. These come from the operating system, so the user has already told you (Media Queries Beyond Width). forced-colors: activemeans 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, andforced-color-adjustlets 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
matchMediacosts one listener. - Animating
transformandopacityis 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
!importantor 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
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.
| What | Against what | Why it is missed | Where to check it |
|---|---|---|---|
| Body text | Its own surface, in light and dark mode | Checked in one theme only | Token pair enumeration in CI (Design Tokens) |
| Secondary and disabled text | The same surface | "It is meant to be quiet" treated as exempt | Same, with the muted tokens included |
| Placeholder text | The input background | Treated as decoration rather than as content | Manually, plus removing it as a label entirely |
| Focus indicator | Every background it can land on: hover, selected row, coloured button, dark mode, forced colours | Only checked on the default page background | Force :focus-visible in devtools across states |
| Icon-only controls | Their button surface | Icons are assumed to be exempt because they are not text | Non-text contrast requirements in the current WCAG version |
| Chart series and status dots | The plot background and each other | Distinguishability between series is a separate question from contrast with the background | Colour-vision simulation plus redundant encoding |
| Text over an image or gradient | The worst pixel underneath it | Automated checkers decline to answer here | A 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.
<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. --><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.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Windows high-contrast mode is on | The focus ring is invisible | The indicator was drawn with box-shadow, which forced-colours discards | Use outline, and set outline-color to a system keyword inside @media (forced-colors: active). |
| Windows high-contrast mode is on | Icon buttons are blank squares | Icons were CSS background-images, which are not painted in forced colours | Use inline SVG with currentColor, or real text. |
| Reduced motion is set | A panel never appears | A global animation: none rule removed the reveal animation that also set the final state | Reduce specific transitions; never blanket-disable animation that carries state. |
| Reduced motion is set | The page still parallaxes and the carousel still autoplays | Only CSS was gated; the JavaScript effects ignore the preference | matchMedia 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 appears | Fixed pixel heights and widths with overflow: hidden | Size with content-relative units and let containers grow (Responsive Typography). |
| A dense table on a phone | Users hit Delete instead of Edit | Adjacent icon targets with almost no spacing | Increase the hit area and the spacing between adjacent actions; separate destructive actions from routine ones. |
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 than19 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 with28 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-motionby 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-shadowfocus rings withoutline, 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: hiddenare 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
- 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: noneglobally, 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.
- 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).
- "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
- 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-colorsand 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.
- 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).
- 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: activeand substitutes the palette, macOS "Increase contrast" surfaces asprefers-contrastwithout 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-transparencyand forforced-color-adjustalso differs between engines.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — 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-motionandforced-colorsso a disappearing focus ring fails a build.