Respecting Reduced Motion Preferences
What This Means
The prefers-reduced-motion media query allows users to indicate they prefer minimal animation and motion on web pages. Users with vestibular disorders, motion sickness, or attention disorders can experience nausea, dizziness, or seizures from excessive animations, parallax effects, and auto-playing videos.
Common Motion Issues
Problematic animations:
- Parallax scrolling effects
- Auto-scrolling carousels
- Large moving backgrounds
- Spinning/rotating elements
- Bouncing or shaking animations
- Sudden movements or zooms
- Flickering or strobing effects
User Preference:
- Users can enable "Reduce Motion" in their OS
- Browsers expose this via
prefers-reduced-motionmedia query - Websites should respect this preference
Impact on Your Business
Accessibility Impact:
- WCAG 2.1 Level AAA (2.3.3 Animation from Interactions)
- WCAG 2.2 Level AAA (2.3.2 Three Flashes)
- Users with vestibular disorders get physically ill
- Can trigger seizures in epileptic users
- Causes disorientation and nausea
- Creates anxiety for sensitive users
Legal Compliance:
- Required for Level AAA compliance
- Increasingly expected for Level AA
- Common in accessibility lawsuits
- Some jurisdictions requiring it
User Impact:
- 35% of adults experience motion sickness
- 15-35% of people have vestibular disorders
- Growing awareness and adoption of reduced motion
- Affects productivity and comfort
Business Consequences:
- Physical illness forces users to leave site
- Negative reviews and complaints
- Lost conversions from affected users
- Brand damage from ignoring accessibility
How to Diagnose
Method 1: Enable Reduced Motion on Your System
macOS:
- System Preferences → Accessibility
- Display → Reduce motion
- Check the box
Windows 10/11:
- Settings → Ease of Access → Display
- Show animations in Windows → Off
iOS:
- Settings → Accessibility
- Motion → Reduce Motion → On
Android:
- Settings → Accessibility
- Remove animations → On
After enabling, test your site:
- Animations should be reduced or removed
- Parallax effects should be static
- Auto-playing videos should pause
- Transitions should be instant or minimal
Method 2: Test with DevTools
Chrome DevTools:
- Open DevTools (
F12) - Press
Cmd/Ctrl + Shift + P - Type "Rendering"
- Select "Show Rendering"
- Find "Emulate CSS media feature prefers-reduced-motion"
- Select "prefers-reduced-motion: reduce"
Firefox DevTools:
- Open DevTools (
F12) - Click three dots menu
- Settings → Advanced
- Check "prefers-reduced-motion: reduce"
What to Look For:
- Animations should stop or minimize
- Parallax effects should disappear
- Transitions should be instant
- Auto-play should be disabled
Method 3: Use Accessibility Audit Tools
axe DevTools:
- Install axe DevTools Extension
- Run scan
- Look for motion-related issues
Manual code review:
/* Search your CSS for animations without reduced motion support */
@keyframes slideIn { }
.parallax { transform: translateY() }
transition: all 0.5s;
Method 4: JavaScript Detection
Check in browser console:
// Check if user prefers reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
console.log('User prefers reduced motion:', prefersReducedMotion);
// Listen for changes
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
console.log('Motion preference changed:', e.matches);
});
Method 5: Manual Visual Testing
- Enable reduced motion on your device
- Navigate through your site
- Look for:
- Animations that still play
- Parallax effects still active
- Auto-scrolling elements
- Flickering or flashing
- Spinning elements
General Fixes
Fix 1: Disable Animations for Reduced Motion
Basic implementation:
/* Define animations normally */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Disable for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.fade-in {
animation: none;
}
}
Better approach - respect motion preference globally:
/* Disable ALL animations and transitions */
@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;
}
}
Selective approach (recommended):
/* Keep safe animations, remove problematic ones */
@media (prefers-reduced-motion: reduce) {
/* Remove large movements */
.parallax,
.slide-in,
.zoom-effect {
animation: none !important;
transform: none !important;
}
/* Keep subtle fades (safe) */
.fade {
animation-duration: 0.1s;
}
/* Instant transitions */
.transition {
transition-duration: 0.01ms;
}
/* Disable auto-scroll */
.carousel {
scroll-behavior: auto;
}
}
Fix 2: Handle Parallax Effects
Disable parallax scrolling:
/* Parallax effect */
.parallax-bg {
background-attachment: fixed;
background-position: center;
transform: translateY(var(--scroll-position));
}
/* Disable for reduced motion */
@media (prefers-reduced-motion: reduce) {
.parallax-bg {
background-attachment: scroll;
transform: none;
}
}
// JavaScript parallax
const parallaxElements = document.querySelectorAll('.parallax');
// Check motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
window.addEventListener('scroll', () => {
// Don't apply parallax if user prefers reduced motion
if (prefersReducedMotion) return;
parallaxElements.forEach(el => {
const scrolled = window.pageYOffset;
el.style.transform = `translateY(${scrolled * 0.5}px)`;
});
});
Fix 3: Respect Motion in JavaScript Animations
Check preference before animating:
// Utility function to check motion preference
function shouldReduceMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Example: Smooth scroll
function scrollToSection(target) {
if (shouldReduceMotion()) {
// Jump instantly
target.scrollIntoView({ behavior: 'auto' });
} else {
// Smooth animation
target.scrollIntoView({ behavior: 'smooth' });
}
}
// Example: Carousel auto-advance
function setupCarousel() {
const carousel = document.querySelector('.carousel');
if (!shouldReduceMotion()) {
// Only auto-advance if motion is allowed
setInterval(() => {
advanceCarousel();
}, 5000);
}
// Still allow manual navigation
}
// Example: Animated counter
function animateCounter(element, target) {
if (shouldReducedMotion()) {
// Show final value immediately
element.textContent = target;
return;
}
// Animate count-up
let current = 0;
const increment = target / 100;
const timer = setInterval(() => {
current += increment;
if (current >= target) {
element.textContent = target;
clearInterval(timer);
} else {
element.textContent = Math.floor(current);
}
}, 20);
}
Fix 4: Handle Video and GIF Animations
Pause auto-playing animations:
<video
id="background-video"
autoplay
muted
loop
playsinline
>
<source src="background.mp4" type="video/mp4">
</video>
<script>
// Pause video if user prefers reduced motion
const video = document.getElementById('background-video');
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
function handleMotionPreference(e) {
if (e.matches) {
video.pause();
} else {
video.play();
}
}
// Check initial preference
handleMotionPreference(prefersReducedMotion);
// Listen for changes
prefersReducedMotion.addEventListener('change', handleMotionPreference);
</script>
Replace animated GIFs with static images:
<picture>
<!-- Static image for reduced motion -->
<source
srcset="image-static.jpg"
media="(prefers-reduced-motion: reduce)"
>
<!-- Animated GIF for normal motion -->
<img src="image-animated.gif" alt="Description">
</picture>
Fix 5: Create Motion-Safe Alternatives
Design with motion preference in mind:
/* Default: subtle animation */
.button {
transition: background-color 0.2s, transform 0.2s;
}
.button:hover {
transform: scale(1.05);
}
/* Reduced motion: instant color change, no transform */
@media (prefers-reduced-motion: reduce) {
.button {
transition: background-color 0.01ms;
}
.button:hover {
transform: none;
}
}
/* Base styles - no motion */
.card {
opacity: 1;
transform: none;
}
/* Add motion only if user allows */
@media (prefers-reduced-motion: no-preference) {
.card {
opacity: 0;
transition: opacity 0.5s, transform 0.5s;
}
.card.visible {
opacity: 1;
transform: translateY(0);
}
}
Fix 6: Implement CSS Custom Properties
Make animation duration controllable:
:root {
--animation-duration: 0.3s;
--transition-duration: 0.2s;
}
@media (prefers-reduced-motion: reduce) {
:root {
--animation-duration: 0.01ms;
--transition-duration: 0.01ms;
}
}
/* Use variables throughout */
.element {
transition: all var(--transition-duration);
animation-duration: var(--animation-duration);
}
Fix 7: Provide User Controls
Let users override motion settings:
<div class="motion-controls">
<label>
<input
type="checkbox"
id="reduce-motion"
aria-label="Reduce motion and animations"
>
Reduce motion
</label>
</div>
<script>
const checkbox = document.getElementById('reduce-motion');
const body = document.body;
// Check system preference
const systemPrefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Load saved preference or use system preference
const savedPreference = localStorage.getItem('reduceMotion');
const shouldReduce = savedPreference !== null
? savedPreference === 'true'
: systemPrefersReduced;
// Set initial state
checkbox.checked = shouldReduce;
if (shouldReduce) {
body.classList.add('reduce-motion');
}
// Handle user changes
checkbox.addEventListener('change', (e) => {
if (e.target.checked) {
body.classList.add('reduce-motion');
localStorage.setItem('reduceMotion', 'true');
} else {
body.classList.remove('reduce-motion');
localStorage.setItem('reduceMotion', 'false');
}
});
</script>
<style>
/* Apply reduced motion when class present */
.reduce-motion *,
.reduce-motion *::before,
.reduce-motion *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
</style>
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing reduced motion support:
Enable reduced motion:
- Turn on in your OS settings
- Verify animations stop/minimize
- Check all page types
Test with DevTools:
- Use "Emulate prefers-reduced-motion"
- Toggle between reduce/no-preference
- Verify behavior changes
Test all animations:
- Parallax effects
- Carousels
- Transitions
- Hover effects
- Page load animations
Check JavaScript:
- Auto-scroll features
- Animated counters
- Video autoplay
- Smooth scroll
Real device testing:
- Test on iPhone with Reduce Motion on
- Test on Android
- Verify no nausea-inducing effects
Common Mistakes
- Ignoring prefers-reduced-motion - Not checking at all
- Only fixing some animations - Need to check all motion
- Making site non-functional - Some users need to see changes
- Forgetting JavaScript animations - CSS isn't the only source
- Not testing with real users - Hard to know what's problematic
- Removing all feedback - Keep instant visual feedback
- Parallax everywhere - Very problematic for vestibular disorders
- Auto-scrolling carousels - Respect motion preference
- Flashing/strobing effects - Can trigger seizures
- Not providing controls - Let users decide
Safe Animations
Animations that are generally safe:
- Fade in/out (opacity only)
- Color changes
- Static position changes
- Instant state changes
Problematic animations to avoid:
- Large movements (sliding, bouncing)
- Rotation/spinning
- Scaling (especially large scale)
- Parallax scrolling
- Auto-scrolling
- Zooming effects
- Shaking/vibrating