Reduced motion and animations
Animations can cause serious physical discomfort for people with vestibular disorders, epilepsy, or motion sensitivity. WCAG requires that motion can be paused, stopped, or disabled.
Who is affected
- Vestibular disorders — Parallax scrolling, zooming animations, and sliding transitions can cause vertigo, nausea, and migraines.
- Epilepsy — Flashing content faster than 3 times per second can trigger seizures (WCAG 2.3.1).
- ADHD and cognitive disabilities — Constant motion is distracting and makes it harder to focus on content.
- Motion sickness — Even subtle animations can cause discomfort during extended use.
prefers-reduced-motion
Users can enable “Reduce motion” in their operating system settings. CSS can detect this :
@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;
}
}Note : Setting duration to
0.01msinstead of0sensuresanimationendandtransitionendevents still fire. This prevents JavaScript that relies on these events from breaking.
Progressive enhancement approach
Instead of removing motion for those who need it, add motion only for those who want it :
.card {
opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
.card {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
}
}This way, animations are the enhancement, not the default.
JavaScript detection
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) {
carousel.stopAutoPlay();
}
prefersReducedMotion.addEventListener('change', (event) => {
if (event.matches) {
carousel.stopAutoPlay();
} else {
carousel.startAutoPlay();
}
});Auto-playing content
WCAG 2.2.2 requires that auto-moving content can be paused, stopped, or hidden :
<div class="banner" role="region" aria-label="Announcements">
<div class="banner__content" id="banner-content">
Special offer: 20% off all plans
</div>
<button aria-label="Pause announcements" id="pause-btn">⏸</button>
</div>const banner = document.getElementById('banner-content');
const pauseBtn = document.getElementById('pause-btn');
let isPaused = false;
pauseBtn.addEventListener('click', () => {
isPaused = !isPaused;
banner.style.animationPlayState = isPaused ? 'paused' : 'running';
pauseBtn.textContent = isPaused ? '▶' : '⏸';
pauseBtn.setAttribute('aria-label',
isPaused ? 'Resume announcements' : 'Pause announcements'
);
});Safe vs unsafe animations
Safe (usually okay)
- Opacity fades
- Color transitions
- Small-scale transforms (subtle hover effects)
- Border and shadow changes
Potentially harmful
- Parallax scrolling
- Large-scale zooming
- Full-page sliding transitions
- Spinning or rotating elements
- Background video loops
Dangerous (WCAG 2.3.1 failure)
- Content flashing more than 3 times per second
- Large areas of flashing red
- Strobing effects
Loading spinners
Replace spinning animations with progress indicators when reduced motion is enabled :
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: #818cf8;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
border-style: dotted;
border-top-color: #818cf8;
opacity: 0.8;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}Scroll-linked animations
The scroll-timeline CSS feature should also respect motion preferences :
@media (prefers-reduced-motion: no-preference) {
.parallax-section {
animation: parallax linear;
animation-timeline: scroll();
}
}Resources
If you found this helpful, share it with someone who's building for the web.