Respecting Motion Preferences with prefers-reduced-motion
Every animation you have written so far runs unconditionally — for every user, on every device, regardless of their preferences. Some users need that to stop.
People with vestibular disorders, migraines, epilepsy, and other conditions can experience nausea, disorientation, or seizures from animation and motion on screen. Every major operating system includes an accessibility setting — “Reduce Motion” on macOS and iOS, “Remove Animations” on Windows, “Disable Animations” on Android — that signals to apps and browsers that the user prefers less motion. CSS exposes this signal through the prefers-reduced-motion media query.
Wrapping your animations in this query is not optional polish. It is the last step before animations are production-ready.
The media query
Section titled “The media query”prefers-reduced-motion accepts two values:
| Value | Meaning |
|---|---|
no-preference | The user has not enabled a reduce-motion setting — motion is acceptable |
reduce | The user has enabled reduce-motion — minimize or eliminate non-essential motion |
@media (prefers-reduced-motion: reduce) { /* styles applied when the user prefers reduced motion */}
@media (prefers-reduced-motion: no-preference) { /* styles applied when the user has no motion preference */}Two approaches
Section titled “Two approaches”There are two ways to structure your animations around this query.
The opt-out pattern (override on reduce)
Section titled “The opt-out pattern (override on reduce)”Write animations without any media query, then add a reduce block that overrides them:
/* Runs for everyone by default */.hero-heading { animation: slideUpFade 600ms ease-out both;}
/* Removed for reduce-motion users */@media (prefers-reduced-motion: reduce) { .hero-heading { animation: none; }}This is the easier pattern to apply to an existing stylesheet — your current rules stay unchanged, and the reduce block sits at the bottom as an override layer.
The opt-in pattern (animate only on no-preference)
Section titled “The opt-in pattern (animate only on no-preference)”Write animations inside a no-preference query. No animation runs unless the user has not requested reduced motion:
@media (prefers-reduced-motion: no-preference) { .hero-heading { animation: slideUpFade 600ms ease-out both; }}This approach is considered better practice because it starts from a safe baseline — non-animating — and only adds motion when the user is known to tolerate it. New animations you add are automatically scoped. The downside is more nesting and a larger refactor if you are retrofitting an existing stylesheet.
For this lesson you will use the opt-out pattern, since you have already written the animation rules and adding a single reduce override block is the correct way to handle this in an existing stylesheet.
What “reduce” means in practice
Section titled “What “reduce” means in practice”prefers-reduced-motion: reduce does not mean “remove all animation.” It means “remove animation that involves significant motion.” The distinction matters:
Remove or disable:
- Sliding transitions (
translateX,translateY) - Scaling (
scale) — especially bouncing or pulsing variations - Rotating elements
- Parallax effects
- Looping animations
Generally acceptable even with reduce:
- Opacity changes (fading in, fading out) — these do not involve spatial motion
- Color transitions
- Very short, subtle state-change transitions (100ms or less)
Opacity-only fades are broadly considered safe because they do not create a sense of spatial movement. A heading that fades in at opacity: 0 → 1 is very different from one that simultaneously slides 24px upward.
Applying prefers-reduced-motion to the STO site
Section titled “Applying prefers-reduced-motion to the STO site”Add this block at the very end of your /* === Animations === */ section in style.css:
@media (prefers-reduced-motion: reduce) { /* Hero entrance — replace slide-up-fade with opacity-only fade */ .hero-heading, .hero-subheading, .hero-cta { animation: fadeIn 400ms ease-out both; }
/* Tour card stagger — remove entrance animation entirely */ .tours-grid .tour-card, .tours-grid .tour-card:nth-child(2), .tours-grid .tour-card:nth-child(3), .tours-grid .tour-card:nth-child(4), .tours-grid .tour-card:nth-child(5), .tours-grid .tour-card:nth-child(6) { animation: none; }}Breaking down each decision:
Hero elements — The slideUpFade animation combines opacity and translateY. For reduced-motion users, replace it with fadeIn (opacity only). This preserves the page-load reveal effect — the content is not just snapping in instantly — while removing the spatial motion. All three hero elements get the same fadeIn, so the stagger collapses: they all appear together rather than in sequence. A collapsed stagger is fine; a staggered slide is what causes the problem.
Tour cards — The staggered slide-up is removed entirely. Cards appear immediately at their natural position on page load. No fadeIn replacement is needed here — the cards are not the primary content reveal, and instant appearance is a better experience than a non-staggered fade when the motion rationale (cascading sequence) is gone.
Disabling existing transitions
Section titled “Disabling existing transitions”The hover lift and image zoom on tour cards (transition: transform) are also a form of motion. These are triggered by user interaction, which is generally considered lower risk than auto-playing animation — the user is in control of the motion by moving their cursor. Most accessibility guidelines focus on auto-playing motion as the primary concern.
That said, if you want to remove transitions as well for reduce-motion users, add:
@media (prefers-reduced-motion: reduce) { .tour-card, .tour-card-image { transition: none; }}Whether to include this is a judgment call. The WAI-ARIA specification and WCAG 2.1 Success Criterion 2.3.3 (AAA) recommend it; many production sites stop at auto-playing animations and leave interaction transitions intact. For STO, either choice is defensible. Include it if you want to follow the stricter interpretation.
Testing your implementation
Section titled “Testing your implementation”You cannot test prefers-reduced-motion by looking at the CSS alone — you need to either enable the OS setting or use DevTools to simulate it.
Chrome DevTools:
- Open DevTools (F12)
- Open the Rendering panel: More tools → Rendering (or Command Palette → Show Rendering)
- Find “Emulate CSS media feature prefers-reduced-motion” and set it to
reduce - Reload the page
macOS: System Settings → Accessibility → Display → Reduce Motion (toggle on)
Windows: Settings → Accessibility → Visual effects → Animation effects (toggle off)
With the setting active, reload index.html. The hero should fade in without any sliding motion. The tour cards should appear immediately. If any sliding or scaling animation still runs, check that the selector in your reduce block matches the selector in the original rule — specificity mismatches are the most common cause of reduce overrides not applying.
Exercise
Section titled “Exercise”-
Add the
prefers-reduced-motion: reduceblock above tostyle.css. -
Use Chrome DevTools to emulate
prefers-reduced-motion: reduce. Reloadindex.htmland confirm:- The hero heading, subheading, and CTA fade in without sliding
- All three elements fade in together (no stagger — the stagger relied on the slide)
- The featured tour cards appear immediately at their natural position
-
Switch the emulated preference back to
no-preference(or clear it). Reload and confirm the original staggered animations are restored. -
Open
tours.htmlwithreduceemulated. Confirm the tour cards appear immediately with no animation. -
Optional: Add the
transition: nonerules for.tour-cardand.tour-card-imageinside thereduceblock. Hover over a card. The lift and image zoom should no longer occur.
prefers-reduced-motion: reducefires when the user has enabled a reduce-motion setting in their OS.no-preferencefires when they have not.- The opt-out pattern keeps your existing animation rules unchanged and adds overrides in a
reduceblock at the end of the stylesheet. The opt-in pattern wraps all animations inno-preferencefrom the start — safer default, larger refactor for existing code. - Opacity-only fades are generally safe for reduced-motion users. Spatial motion (
translate,scale,rotate) and looping animations are the primary targets for removal. - For the STO hero: replace
slideUpFadewithfadeInunderreduce— preserves reveal, removes motion. - For the STO tour cards: remove the animation entirely under
reduce— cards appear immediately. - Test with Chrome DevTools Rendering panel → “Emulate CSS media feature prefers-reduced-motion” before enabling the OS setting.
Lesson 08 is the module recap — a consolidated reference for every concept covered in CSS M09, a summary of the STO animations you have built, and a preview of how JavaScript Foundations extends this work.