How to Implement prefers-reduced-motion Correctly: CSS and JavaScript Guide

Wrap motion-heavy CSS animations in @media (prefers-reduced-motion: reduce) overrides and use a useReducedMotion hook to gate JavaScript-driven animations, replacing transforms with opacity or color transitions.

The prefers-reduced-motion media feature is essential for accessible web interfaces that respect user accessibility settings. According to the emilkowalski/skills repository standards, properly handling this setting prevents motion sickness while preserving necessary visual feedback for state changes. This guide covers the implementation patterns found in skills/review-animations/STANDARDS.md and related skill files.

CSS-Only Implementation with Media Queries

The most reliable method to honor user preferences is to wrap animation properties in a media query that only applies when the user has not requested reduced motion. When reduce is detected, replace heavy transforms with gentler effects like opacity or color changes.

In skills/review-animations/STANDARDS.md, the recommended pattern shows how to maintain visual feedback while removing problematic motion:

/* Default animation (used when reduced-motion is NOT requested) */
.element {
  animation: slideIn 300ms ease-out forwards;
}

/* Reduced-motion override – keep only opacity/color changes */
@media (prefers-reduced-motion: reduce) {
  .element { animation: fade 0.2s ease; } /* keep opacity/color, drop transform-based motion */
}

This approach ensures that users who require reduced motion still receive state-change feedback through subtle transitions rather than position-shifting transforms.

JavaScript Detection in React

For JavaScript-driven animations using libraries like Framer Motion or Spring, you need runtime detection. The matchMedia API allows you to query the preference and create a reactive hook.

Create a useReducedMotion hook as shown in the skills repository:

// src/hooks/useReducedMotion.ts
import { useEffect, useState } from 'react';

export function useReducedMotion(): boolean {
  const [reduce, setReduce] = useState(() =>
    typeof window !== 'undefined'
      ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
      : false,
  );

  useEffect(() => {
    const media = window.matchMedia('(prefers-reduced-motion: reduce)');
    const handler = () => setReduce(media.matches);
    media.addEventListener('change', handler);
    return () => media.removeEventListener('change', handler);
  }, []);

  return reduce;
}

Use this hook to conditionally adjust animation values, as demonstrated in skills/review-animations/STANDARDS.md:

import { useReducedMotion } from '@/hooks/useReducedMotion';

function Drawer() {
  const reduce = useReducedMotion();
  const closedX = reduce ? 0 : '-100%';   // Skip slide when reduced-motion is on

  return (
    <motion.div
      animate={{ x: closedX }}
      transition={{ duration: reduce ? 0.2 : 0.3, ease: 'easeOut' }}
    />
  );
}

Gating Hover Effects for Input Devices

Not all devices support hover, and hover-driven motion can be particularly problematic for touch users. Combine prefers-reduced-motion with hover and pointer media features to limit effects to appropriate devices.

@media (hover: hover) and (pointer: fine) {
  .button:hover { transform: scale(1.05); } /* only on true hover devices */
}

This pattern from the repository's accessibility guidelines ensures that scale transforms and other hover effects only activate on devices with fine pointer control, avoiding unexpected motion on touchscreens.

The "Gentle, Not Zero" Design Principle

The emilkowalski/skills repository emphasizes that reduced motion does not mean removing all animations. According to skills/review-animations/STANDARDS.md, you should keep transitions that aid comprehension while removing movement that causes discomfort.

“Reduced motion means fewer and gentler animations, not zero — keep transitions that aid comprehension, remove movement/position changes.”

When prefers-reduced-motion: reduce is active:

  • Remove: Large position changes, parallax effects, spinning loaders
  • Keep: Subtle opacity fades, color transitions, and scale changes that indicate state

Testing Your Implementation

Manual Testing

Toggle Reduced Motion in your operating system settings, or use Chrome DevTools:

  1. Open DevTools → Rendering tab
  2. Enable "Emulate CSS media feature prefers-reduced-motion: reduce"

Automated Testing

In Jest or React Testing Library, mock window.matchMedia to return matches: true and assert that your components render with fallback styles or values:

// Mock matchMedia for reduced motion tests
window.matchMedia = jest.fn().mockImplementation(query => ({
  matches: query === '(prefers-reduced-motion: reduce)',
  addEventListener: jest.fn(),
  removeEventListener: jest.fn(),
}));

Summary

  • Use @media (prefers-reduced-motion: reduce) in CSS to override transform-based animations with opacity or color alternatives, as specified in skills/review-animations/STANDARDS.md.
  • Implement a useReducedMotion hook using window.matchMedia to gate JavaScript animations in React components.
  • Combine with hover media features to ensure motion only occurs on appropriate input devices.
  • Follow the "gentle, not zero" principle by maintaining subtle transitions that convey state changes without causing motion sickness.
  • Test manually via OS settings or DevTools, and automate with mocked matchMedia in your test suite.

Frequently Asked Questions

What is prefers-reduced-motion and why does it matter?

The prefers-reduced-motion CSS media feature detects when a user has requested reduced motion in their operating system settings. It matters for accessibility because unexpected animations can trigger vestibular disorders, causing dizziness or nausea for sensitive users. According to skills/review-animations/SKILL.md, respecting this preference is a core accessibility requirement for modern web applications.

Should I remove all animations when prefers-reduced-motion is enabled?

No. As documented in skills/review-animations/STANDARDS.md, reduced motion should mean "gentler, not zero." Remove problematic transforms like slides and parallax, but keep subtle opacity and color transitions that help users understand state changes. The skills/apple-design/SKILL.md file specifically recommends replacing slide animations with cross-fades rather than removing feedback entirely.

How do I detect prefers-reduced-motion in JavaScript without React?

Use the native matchMedia API: const media = window.matchMedia('(prefers-reduced-motion: reduce)'); Check media.matches for the current state and attach a change event listener to detect live updates. This works in vanilla JavaScript or any framework, though the React hook pattern in skills/review-animations/STANDARDS.md provides a convenient wrapper for component-based architectures.

Which repository files contain the official standards for reduced motion?

The primary reference is skills/review-animations/STANDARDS.md, which contains the CSS media query patterns and React hook examples. Additional context appears in skills/review-animations/SKILL.md for accessibility expectations, skills/improve-animations/AUDIT.md for audit checklists, and skills/emil-design-eng/SKILL.md for design implementation guidance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →