Snippets / Accessibility /

Motion-Safe Animation

Write animations inside @media (prefers-reduced-motion: no-preference) — motion is opt-in by default, so reduced-motion users never need overrides. Always safe; always correct.

Staggered entrance (only animates if motion is OK)
Card one — rises in if motion is allowed
Card two — delayed 100ms
Card three — delayed 200ms
Button — arrow moves only when motion is safe; opacity always transitions
Widely Supported
a11yno-js

Quick implementation

/* SAFE PATTERN: write motion inside no-preference query
   (motion is opt-in — reduced-motion users never need overrides) */

@media (prefers-reduced-motion: no-preference) {
  .card-enter {
    animation: rise 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Hover: only move elements when motion is permitted */
@media (prefers-reduced-motion: no-preference) {
  .btn .arrow {
    transition: transform 0.2s ease;
  }
  .btn:hover .arrow {
    transform: translateX(4px);
  }
}

/* Opacity transitions are always acceptable — no spatial motion */
.btn {
  transition: opacity 0.15s ease;
}
.btn:hover { opacity: 0.88; }

/* AVOID PATTERN (requires per-animation overrides):
.card-enter { animation: rise 0.5s ... }
@media (prefers-reduced-motion: reduce) {
  .card-enter { animation: none; }  — easy to forget
} */

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are an accessibility-focused CSS developer. Explain and implement the motion-safe animation pattern.

Requirements:
1. Show the "opt-in" approach: all transform/translate/scale animations live INSIDE @media (prefers-reduced-motion: no-preference). Reduced-motion users get no animation without any override needed.
2. Contrast with the "opt-out" approach: writing animations globally and overriding with @media (prefers-reduced-motion: reduce). Explain why opt-in is safer — opt-out requires you to remember every animation when writing overrides.
3. Implement staggered card entrance animations (animation-delay on nth-child) inside the no-preference query.
4. Show which transitions are always safe (never require a reduced-motion guard):
   - opacity transitions (no spatial movement)
   - color transitions
   - box-shadow transitions that don't involve scale
5. Show which transitions always need a guard:
   - transform: translate/scale/rotate
   - animation with @keyframes involving spatial movement
   - scroll-driven animations with significant parallax

Constraints:
- WCAG 2.3.3 (AAA) covers animations triggered by interaction that last more than 5 seconds. Below 5 seconds is technically exempt at AA but should still be guarded for user comfort.
- Do not put @keyframes inside the media query — keyframes themselves are harmless; it's the animation property that applies them. Put the animation property inside the query.
- The Tailwind equivalent: the motion-safe: variant applies a class only when motion is permitted (e.g., motion-safe:animate-bounce).

Output CSS showing both the opt-in and opt-out approaches side by side, clearly labeled, then the full recommended implementation.

Why this matters in 2026

The prefers-reduced-motion: reduce approach — writing animations globally and adding overrides — scales poorly. As a codebase grows, every new animation requires a matching reduced-motion override that is easy to forget. The inverse pattern — writing motion only inside prefers-reduced-motion: no-preference — makes motion opt-in by default. A user with reduced-motion enabled gets a clean, motionless UI automatically, and there is nothing to remember or audit when adding new animations.

The logic

Not all transitions are equal from a vestibular perspective. Spatial movement — translate, scale, rotate, parallax — is the primary trigger for motion sickness and vestibular discomfort. Opacity and color transitions involve no spatial movement and are generally considered safe for all users. The practical rule: gate any animation that moves an element across the screen with a no-preference query; leave opacity and color transitions ungated. This distinction also improves performance — opacity and color are composited cheaply, while spatial transforms on large elements can require repaints.

Accessibility & performance

The no-preference pattern has a useful side effect: browsers that do not support prefers-reduced-motion will never see the animation (it is inside an unsupported query). This is the most conservative possible default — no animation until the browser explicitly signals motion is welcome. For scroll-driven animations and View Transitions, the same pattern applies: place view-transition-name and scroll-driven animation declarations inside the no-preference query so they are inert for reduced-motion users without any additional override.