# How to Implement the Prefers-Reduced-Motion Accessibility Pattern in CSS and JavaScript

> Learn to implement the prefers-reduced-motion accessibility pattern using CSS and JavaScript. Disable animations for users sensitive to motion, improving user experience.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-08-02

---

**The `prefers-reduced-motion` accessibility pattern combines a CSS media query with a JavaScript hook to conditionally disable motion-heavy animations while preserving essential visual feedback.**

The `emilkowalski/skills` repository establishes a battle-tested approach for respecting users' motion preferences. This pattern splits responsibility between **declarative CSS** (disabling problematic animations) and **imperative JavaScript** (conditionally adjusting animation values), ensuring all animation sources stay synchronized with the OS-level setting.

## CSS Implementation: The Media Query Foundation

The core of the pattern lives in [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md), where the canonical rule strips **transform-based motion** while keeping opacity and color transitions intact.

### The Base Pattern

```css
/* Full-motion experience */
.element {
  animation: slideIn 300ms ease-out forwards;
  transform: translateX(0);
  transition: transform 300ms ease;
}

/* Reduced-motion fallback */
@media (prefers-reduced-motion: reduce) {
  .element {
    animation: fade 0.2s ease;  /* keep opacity/color feedback */
    transform: none;            /* remove position/shape changes */
    transition: none;
  }
}

```

According to the repository standards at lines 163-165, this approach avoids **motion sickness triggers** (rapid position changes, scaling, rotation) without creating a "jarring" instant state change. The fade animation provides context that something happened—critical for comprehension—without the vestibular disruption.

### Picker Component Example

The [`skills/prototype/PICKER.md`](https://github.com/emilkowalski/skills/blob/main/skills/prototype/PICKER.md) file (lines 70-72) demonstrates component-level application:

```css
.proto-picker-highlight {
  transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1),
              width 250ms cubic-bezier(0.23, 1, 0.32, 1);
}

@media (prefers-reduced-motion: reduce) {
  .proto-picker-highlight {
    transition: none;
  }
}

```

This disables the **slide transition** for the picker's highlight indicator when reduced motion is requested.

## JavaScript Implementation: The useReducedMotion Hook

For JavaScript-driven animations—tweens, spring physics, or library-controlled motion—the repository provides a `useReducedMotion` hook. This ensures **imperative code** respects the same preference as CSS.

### Hook Implementation

```javascript
import { useEffect, useState } from 'react';

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

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

  return reduce;
}

```

The hook uses **`window.matchMedia`** for initial detection and attaches a listener for runtime changes—critical because users can toggle this setting without reloading the page.

### Component Usage Pattern

From [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) lines 71-74, the canonical usage:

```javascript
const reduce = useReducedMotion();
const closedX = reduce ? 0 : '-100%';

```

This pattern—**ternary fallback to static values**—appears throughout the codebase as the standard way to adapt JS-driven motion.

### Full Component Example: Picker Highlight

```javascript
import { useReducedMotion } from './useReducedMotion';

function PickerHighlight({ dataReady, targetRect }) {
  const reduce = useReducedMotion();

  // Skip animated transitions when reduced motion is preferred
  const style = {
    transform: `translateX(${targetRect.left}px)`,
    width: targetRect.width,
    transition:
      dataReady && !reduce
        ? 'transform 250ms cubic-bezier(0.23,1,0.32,1), width 250ms cubic-bezier(0.23,1,0.32,1)'
        : 'none',
  };

  return <div className="proto-picker-highlight" style={style} />;
}

```

Here, `!reduce` gates the transition string—when true, the highlight snaps instantly to its new position rather than sliding.

## Additional Accessibility Guards: Hover Gating

The repository extends the pattern to **input modality** at [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) lines 66-68:

```css
@media (hover: hover) and (pointer: fine) {
  .element:hover {
    transform: scale(1.05);
  }
}

```

This prevents hover-triggered motion on **touch devices** where hover is unreliable. Combined with `prefers-reduced-motion`, this creates defense in depth against unwanted animation.

## Why This Architecture Works

| Concern | Solution |
|--------|----------|
| **User preference** | `@media (prefers-reduced-motion: reduce)` automatically syncs with OS settings—no configuration needed. |
| **Graceful degradation** | Keeps opacity/color feedback for comprehension; removes transform motion that causes vestibular issues. |
| **Cross-source consistency** | CSS and JS consult the same media query, eliminating sync bugs between declarative and imperative animation. |
| **Performance** | Disabling `transform` and `transition` reduces layout-thrashing repaints on low-end devices. |
| **Runtime adaptability** | The JS hook listens for media query changes, responding instantly to setting toggles. |

## Implementation Checklist

1. **Audit existing animations** in [`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md) to identify transform-heavy elements.
2. **Add CSS media queries** wherever animation is defined—preserve only opacity/color in the reduced block.
3. **Implement `useReducedMotion`** in your utilities layer (copy the hook above).
4. **Replace hardcoded values** with conditional logic: `const value = reduce ? staticValue : animatedValue`.
5. **Test** via Chrome DevTools Rendering panel → "Emulate CSS media feature prefers-reduced-motion: reduce".

## Summary

- **`prefers-reduced-motion`** is an OS-level accessibility setting that CSS and JavaScript must both respect.
- The **`@media (prefers-reduced-motion: reduce)`** query in [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) disables transform motion while preserving opacity/color feedback.
- The **`useReducedMotion` hook** provides JavaScript access to the same preference, enabling conditional animation values.
- The **Picker prototype** in [`skills/prototype/PICKER.md`](https://github.com/emilkowalski/skills/blob/main/skills/prototype/PICKER.md) demonstrates production-ready integration of both CSS and JS patterns.
- **Hover gating** with `@media (hover: hover)` prevents false-positive animations on touch devices.

## Frequently Asked Questions

### What exactly does the prefers-reduced-motion media query detect?

The `@media (prefers-reduced-motion: reduce)` query evaluates to true when the user has enabled reduced motion in their operating system settings. On macOS, this is System Preferences → Accessibility → Display → Reduce motion. On iOS, it's Settings → Accessibility → Motion → Reduce Motion. Windows and Android have equivalent settings. The browser surfaces this preference to CSS and JavaScript automatically.

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

No. According to the [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) guidance, you should preserve **opacity fades and color transitions** that provide context without causing motion sickness. Remove or disable **position changes, scaling, rotation, and parallax effects**. The goal is maintaining comprehension cues while eliminating vestibular triggers.

### How do I test prefers-reduced-motion without changing system settings?

Chrome DevTools provides emulation: open the **Rendering** panel (More tools → Rendering), then enable "Emulate CSS media feature prefers-reduced-motion: reduce". Firefox and Safari also support this emulation in their developer tools. For automated testing, Puppeteer and Playwright can override the media feature programmatically via `page.emulateMediaFeatures()`.

### Can third-party animation libraries respect this pattern?

Yes. Pass the `useReducedMotion` boolean to library configuration objects. For Framer Motion, use `<MotionConfig reducedMotion="user">`. For React Spring, conditionally set `{ immediate: reduce }` on your springs. For GSAP, wrap timeline calls in `if (!reduce)` blocks or set `gsap.globalTimeline.timeScale(reduce ? 0 : 1)`.