Contrast, Motion and Button Size: Visual Accessibility
Contrast requirements, information beyond color, high contrast mode, prefers-reduced-motion, target sizes and zoom

Part 5 of 8 in the "Accessible Web in Practice" series. Previous part: Keyboard and Focus: Building a Website You Can Use Without a Mouse.
Low text contrast is the most common accessibility problem on the web: it appears on 83.9% of popular websites. This part covers everything visual — contrast and color, display modes, animation, target size and zoom.
Color, contrast and display modes
Contrast requirements
| What | Minimum (AA) | Criterion |
|---|---|---|
| Normal text | 4.5 : 1 | 1.4.3 |
| Large text (24px and up, or about 18.7px bold) | 3 : 1 | 1.4.3 |
| UI components: input borders, icons, focus indicators, states | 3 : 1 | 1.4.11 |
| Meaningful graphics (chart lines, pie segments) | 3 : 1 | 1.4.11 |
The requirement doesn't apply to disabled controls, logos or purely decorative elements.
Typical problems:
- light gray
#999text on white: only about 2.8 : 1; - placeholders and hints that are too faint;
- white text on a bright brand color;
- text over a photo with no backing;
- a
#dddinput border on white — the field is "invisible" to low-vision users.
You can check contrast right in DevTools: in Chrome, the color picker in the Styles panel shows the contrast ratio with AA/AAA marks. There are standalone tools too, such as the WebAIM Contrast Checker.
If colors live in a design system, define background/text pairs that pass contrast up front and disallow the rest.
Not by color alone
Criterion 1.4.1: color must not be the only way to convey information. About 8% of men have some form of color vision deficiency, and in bright sun or on a poor monitor everyone distinguishes colors worse.
<!-- ❌ Error shown only by a red border -->
<input class="border-red">
<!-- ✅ Color + icon + text -->
<input aria-invalid="true" aria-describedby="err">
<p id="err" class="error"><svg aria-hidden="true">...</svg> Choose a date in the future</p>
Same with links in body text: if a link differs from surrounding text only by color, the contrast between them must be at least 3 : 1, plus an extra cue on hover and focus. The simplest option is to keep the underline.
/* Links in article text keep their underline */
.prose a {
text-decoration: underline;
text-underline-offset: 0.15em;
}
In charts, distinguish lines by more than color: markers, patterns or labels placed right at the lines.
High contrast mode and dark theme
Windows has contrast themes (forced colors mode): the browser replaces all colors with the user's system palette. What breaks:
- icons drawn with backgrounds or
background-imagedisappear; - borders drawn with shadows vanish;
- focus drawn with
box-shadowisn't visible (fix: a transparentoutline, see Keyboard and Focus: Building a Website You Can Use Without a Mouse).
@media (forced-colors: active) {
.icon {
/* SVG using currentColor adapts on its own; for masks use a system color */
background-color: CanvasText;
}
.card {
border: 1px solid CanvasText; /* instead of a shadow */
}
}
You can test it in Chrome DevTools: Rendering → Emulate CSS media feature forced-colors.
A dark theme via prefers-color-scheme needs its own contrast check too — its color pairs are different.
Motion and animation
Parallax, zoom-on-scroll and large screen movements can cause dizziness and nausea in people with vestibular disorders. For people with ADHD, endless animation makes reading harder. Operating systems have a "Reduce motion" setting, and browsers expose it through the prefers-reduced-motion media query.
/* Basic approach: enable animation only for those who haven't asked to reduce it */
@media (prefers-reduced-motion: no-preference) {
.hero {
animation: slide-in 0.6s ease-out;
}
html {
scroll-behavior: smooth;
}
}
Or the reverse — a global safety net:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
In JavaScript (for animation libraries, for example):
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
element.animate(
[{ transform: 'translateY(20px)', opacity: 0 }, { transform: 'none', opacity: 1 }],
{ duration: reduceMotion ? 0 : 400, easing: 'ease-out', fill: 'forwards' }
);
"Reduce motion" doesn't mean "remove everything": a gentle fade is usually fine. Movement, scaling and rotation are what cause problems.
Two more rules:
- Anything that moves for more than 5 seconds (carousels, tickers, background video) needs a pause button (criterion 2.2.2).
- Nothing flashes more than three times per second (2.3.1) — it can trigger seizures.
Target size, gestures and zoom
Target size
WCAG 2.2 criterion 2.5.8 (level AA): an interactive target must be at least 24×24 CSS pixels, or a small target needs enough clear space that a 24px-diameter circle centered on it doesn't overlap neighboring targets. For mobile interfaces, platform guidelines are a better benchmark: Apple recommends 44×44 pt, Material Design 48×48 dp. WCAG level AAA (2.5.5) asks for 44×44 CSS pixels.
You can enlarge the hit area without changing the look:
/* 16px icon, 44px clickable area */
.icon-button {
display: inline-grid;
place-items: center;
min-width: 44px;
min-height: 44px;
padding: 0;
}
/* Or extend the hit area with a pseudo-element */
.small-link {
position: relative;
}
.small-link::after {
content: "";
position: absolute;
inset: -10px;
}
Gestures and dragging
- Complex gestures (pinch, two-finger swipe, drawing a path) need a single-tap alternative (2.5.1). If a map zooms with a pinch, it needs "+" and "−" buttons.
- Dragging (2.5.7, new in 2.2): drag-and-drop list sorting, sliders, drag-to-upload — all need an alternative. For sorting, "Move up" / "Move down" buttons or a "Move to" menu; for uploads, a regular file picker button.
- Activation on release (2.5.2): the action happens on release (
click,pointerup), not on press (pointerdown), so people can "change their mind" by sliding their finger away.
Zoom and reflow
-
Don't disable zoom:
<!-- ❌ --> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <!-- ✅ --> <meta name="viewport" content="width=device-width, initial-scale=1"> -
Reflow (1.4.10): at a width of 320 CSS pixels (that's 400% zoom on a 1280px screen), content must be readable without horizontal scrolling. Responsive layouts usually handle this — check by narrowing the window or zooming in.
-
Font sizes in
rem, notpx, so the browser's font size setting is respected. -
Text spacing (1.4.12): the layout must not break if the user increases line height to 1.5, letter spacing to 0.12em and word spacing to 0.16em. The main enemy is fixed-height blocks (
height: 40px) instead ofmin-height.
The "Accessible Web in Practice" series
- Web Accessibility: Who It Really Serves and Where to Start
- Semantic HTML and Alt Text: The Foundation of an Accessible Website
- Accessible Forms: Labels, Hints, Errors and Autofill
- Keyboard and Focus: Building a Website You Can Use Without a Mouse
- Contrast, Motion and Button Size: Visual Accessibility — you are here
- ARIA and Dynamic Content: When Attributes Help and When They Hurt
- Accessibility in React: IDs, Focus on Route Change and Announcements
- How to Test Accessibility: Automation, Manual Checks and a Checklist
Next part: ARIA and Dynamic Content: When Attributes Help and When They Hurt
Agama Labs builds websites and web applications. Want to check how accessible your site is? Write to us: hello@agamalabs.com


