# How to Implement Reduced Motion Accessibility with useReducedMotion() in Taste-Skill

> Implement reduced motion accessibility in taste-skill using useReducedMotion(). Detect user preferences and disable animations to ensure compliance with accessibility guardrails.

- Repository: [Leon Lin/taste-skill](https://github.com/Leonxlnx/taste-skill)
- Tags: how-to-guide
- Published: 2026-05-31

---

**Call `useReducedMotion()` from `motion/react` at the top of animated components to detect user preferences; when it returns `true`, skip GSAP initialization or set Motion's `initial` prop to `false`, ensuring all animations with **MOTION_INTENSITY** greater than 3 comply with the project's mandatory accessibility guardrails.**

The taste-skill repository enforces strict accessibility standards requiring reduced-motion support for high-intensity animations. By leveraging the **`useReducedMotion()`** hook from the Motion library, developers can detect system-level motion preferences and conditionally disable animations that exceed intensity thresholds. This pattern ensures WCAG 2.1 AAA compliance while maintaining performance for users who prefer static interfaces.

## Understanding the Accessibility Guardrails

The project mandates that any animation with **MOTION_INTENSITY** greater than 3 must respect the user's `prefers-reduced-motion` setting. According to the canonical skeletons defined in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md), this is implemented by importing `useReducedMotion` from `motion/react` and using it as a circuit breaker before initializing animation libraries like GSAP or Motion.

The [`CHANGELOG.md`](https://github.com/Leonxlnx/taste-skill/blob/main/CHANGELOG.md) at line 74 records this decision, confirming the policy is enforced across the codebase.

## Guarding GSAP Animations

For GSAP-based components like **Sticky-Stack** and **Horizontal-Pan**, the hook prevents expensive timeline initialization when users prefer reduced motion.

### Sticky-Stack Implementation

The Sticky-Stack component implements a pin-and-scale carousel that must skip all ScrollTrigger logic when motion is reduced.

```tsx
"use client";
import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";

gsap.registerPlugin(ScrollTrigger);

export function StickyStack({ cards }: { cards: React.ReactNode[] }) {
  const ref = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();               // ← detect preference

  useEffect(() => {
    if (reduce || !ref.current) return;            // ← bail out early
    const ctx = gsap.context(() => {
      const cardEls = gsap.utils.toArray<HTMLElement>(".stack-card");
      cardEls.forEach((card, i) => {
        if (i === cardEls.length - 1) return;
        ScrollTrigger.create({
          trigger: card,
          start: "top top",
          endTrigger: cardEls.at(-1),
          end: "top top",
          pin: true,
          pinSpacing: false,
        });
        gsap.to(card, {
          scale: 0.92,
          opacity: 0.55,
          ease: "none",
          scrollTrigger: {
            trigger: cardEls[i + 1],
            start: "top bottom",
            end: "top top",
            scrub: true,
          },
        });
      });
    }, ref);
    return () => ctx.revert();
  }, [reduce]);                                    // re‑run if system setting changes

  return (
    <div ref={ref} className="relative">
      {cards.map((card, i) => (
        <div
          key={i}
          className="stack-card sticky top-0 min-h-[100dvh] flex items-center justify-center"
        >
          {card}
        </div>
      ))}
    </div>
  );
}

```

### Horizontal-Pan Implementation

Similarly, the Horizontal-Pan component uses the hook to disable vertical-scroll-driven horizontal travel.

```tsx
"use client";
import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";

gsap.registerPlugin(ScrollTrigger);

export function HorizontalPan({ children }: { children: React.ReactNode }) {
  const wrap = useRef<HTMLDivElement>(null);
  const track = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();

  useEffect(() => {
    if (reduce || !wrap.current || !track.current) return;
    const ctx = gsap.context(() => {
      const distance = track.current!.scrollWidth - window.innerWidth;
      gsap.to(track.current, {
        x: -distance,
        ease: "none",
        scrollTrigger: {
          trigger: wrap.current,
          start: "top top",
          end: () => `+=${distance}`,
          pin: true,
          scrub: 1,
          invalidateOnRefresh: true,
        },
      });
    }, wrap);
    return () => ctx.revert();
  }, [reduce]);

  return (
    <section ref={wrap} className="relative overflow-hidden">
      <div ref={track} className="flex h-[100dvh] items-center">
        {children}
      </div>
    </section>
  );
}

```

## Handling Motion Component Animations

For components using the Motion library directly, set the `initial` prop to `false` when `useReducedMotion()` returns `true`.

### Reveal-Stagger with Static Fallback

The **Reveal-Stagger** component demonstrates how to render content instantly when motion is reduced.

```tsx
"use client";
import { motion, useReducedMotion } from "motion/react";

export function RevealStagger({ items }: { items: string[] }) {
  const reduce = useReducedMotion();

  return (
    <ul className="grid gap-6">
      {items.map((item, i) => (
        <motion.li
          key={item}
          initial={reduce ? false : { opacity: 0, y: 24 }}   // ← static when reduced
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, amount: 0.3 }}
          transition={{
            duration: 0.6,
            delay: i * 0.06,
            ease: [0.16, 1, 0.3, 1],
          }}
        >
          {item}
        </motion.li>
      ))}
    </ul>
  );
}

```

## Complementing with CSS Media Queries

While `useReducedMotion()` handles JavaScript animations, pair it with CSS for pure-CSS transitions.

```css
/* Enable animations only when the user allows motion */
@media (prefers-reduced-motion: no-preference) {
  .animate-fade-in {
    animation: fadeIn 0.6s ease-out forwards;
  }
}

/* Explicitly disable any residual animation */
@media (prefers-reduced-motion: reduce) {
  .animate-fade-in {
    animation: none;
  }
}

```

## Summary

- Import `useReducedMotion` from `motion/react` in any component with **MOTION_INTENSITY** > 3.
- Call the hook at component top: `const reduce = useReducedMotion()`.
- Short-circuit GSAP initialization with `if (reduce) return;` to prevent ScrollTrigger and timeline setup.
- Set Motion's `initial` prop to `false` when reduced motion is preferred.
- Reference the canonical skeletons in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) for implementation patterns.
- Add CSS media queries as a fallback for non-JavaScript animations.

## Frequently Asked Questions

### How does `useReducedMotion()` detect user preferences?

The hook queries the `prefers-reduced-motion` media feature from the user's operating system or browser settings. It returns `true` when the user has selected reduced motion in accessibility settings, allowing components to respond immediately without waiting for CSS media queries to apply.

### Where is the reduced-motion guardrail defined in taste-skill?

The mandatory rule requiring reduced-motion support for animations with intensity greater than 3 is documented in [`skills/taste-skill/SKILL.md`](https://github.com/Leonxlnx/taste-skill/blob/main/skills/taste-skill/SKILL.md) under the reduced-motion section. The specification also appears in [`CHANGELOG.md`](https://github.com/Leonxlnx/taste-skill/blob/main/CHANGELOG.md) at line 74, confirming this policy is enforced across all skill implementations.

### Can I use `useReducedMotion()` with animation libraries other than GSAP?

Yes. While the taste-skill examples demonstrate GSAP and Motion integration, the hook works with any animation library. Simply check the `reduce` boolean before initializing animations in Framer Motion, React Spring, or custom requestAnimationFrame loops.

### What happens if I forget to implement the reduced-motion check?

Animations with **MOTION_INTENSITY** > 3 that lack `useReducedMotion()` guards violate the project's accessibility standards and WCAG 2.1 AAA guidelines. The component will animate regardless of user preferences, potentially causing accessibility issues for users with vestibular disorders or motion sensitivity.