How to Implement Reduced Motion Support for Accessibility in Taste-Skill

Taste-Skill mandates that any animation with a MOTION_INTENSITY above 3 must respect the user's prefers-reduced-motion setting by combining the useReducedMotion() hook from motion/react with CSS @media guards.

The Leonxlnx/taste-skill repository enforces strict accessibility guardrails to ensure motion does not create barriers for users with vestibular disorders. Implementing reduced motion support for accessibility is a non-negotiable requirement when working with high-intensity animations within this design system. This guide breaks down the two-layer approach—React-level detection and CSS-level media queries—codified in skills/taste-skill/SKILL.md.

The Motion Intensity Threshold

According to skills/taste-skill/SKILL.md (lines 525–529), Taste-Skill divides motion into intensity levels. Any animation whose MOTION_INTENSITY exceeds 3 must unconditionally respect the user's reduced-motion setting. Motion levels 1–3 are considered static and do not require additional reduced-motion handling, as noted in SKILL.md line 561. This rule applies across the entire codebase and is not optional.

Two-Layer Implementation Strategy

Taste-Skill requires both a React and a CSS layer because projects typically contain a mix of Motion-driven JavaScript animations and pure CSS transitions.

  • React / Framer Motion layer: Use the useReducedMotion() hook from the motion/react package. This hook returns true when the OS-level reduced-motion preference is active, letting components conditionally render static variants or skip animation props entirely.
  • CSS layer: Guard keyframes and transitions inside @media (prefers-reduced-motion: no-preference). Provide a @media (prefers-reduced-motion: reduce) override that disables or replaces the animation with instantaneous state changes.

Applying both layers ensures that no animated path leaks through, regardless of whether the motion originates from a motion.div or a stylesheet.

Step-by-Step Guide to Implement Reduced Motion Support for Accessibility

1. Detect the Preference

Import useReducedMotion inside any animated component. As implemented in Leonxlnx/taste-skill, this hook queries the user's system preference directly:

import { useReducedMotion } from "motion/react";

const prefersReduced = useReducedMotion(); // true → user prefers reduced motion

2. Conditionally Apply Animation Props

When prefersReduced is true, pass animate={false} or render a static variant. For motion intensity levels 1–3, simple hover effects can remain because they fall within the static threshold. High-intensity effects must be gated:

animate={prefersReduced ? staticVariant : animatedVariant}

3. Wrap CSS Animations in Media Queries

Place default animated styles inside @media (prefers-reduced-motion: no-preference) and provide a reduce fallback. This pattern lives in your global stylesheet or CSS modules:

@media (prefers-reduced-motion: no-preference) {
  .fade-in { animation: fadeIn 0.6s ease-out; }
}

@media (prefers-reduced-motion: reduce) {
  .fade-in { animation: none; }
}

4. Avoid Forbidden Animation Patterns

The Performance & Accessibility Guardrails section in SKILL.md (lines 511–514) explicitly bans window.addEventListener('scroll') and manual scrollY calculations. Rely on Motion hooks such as useScroll and useMotionValue, or use CSS scroll-timeline approaches. This fail-fast policy prevents jank and preserves user control over motion.

5. Test the Reduced-Motion Experience

Verify behavior across both states:

  • Confirm the component animates when the OS preference is set to no-preference.
  • Toggle the OS setting to reduce, or simulate it via Chrome DevTools, and verify that all motion collapses to static.
  • Run a Lighthouse Accessibility audit; the reduced-motion check should pass.

Code Examples for Reduced Motion Support

React Component with Motion and Reduced-Motion Fallback

The following component conditionally swaps between a spring animation and an instantaneous static state:

import { motion, useReducedMotion } from "motion/react";

export function Card({ title, children }: { title: string; children: ReactNode }) {
  const prefersReduced = useReducedMotion();

  // Simple static variant for reduced‑motion users
  const staticVariant = { opacity: 1, y: 0 };
  const animatedVariant = {
    opacity: 1,
    y: 0,
    transition: { type: "spring", stiffness: 120 },
  };

  return (
    <motion.article
      initial={{ opacity: 0, y: 20 }}
      animate={prefersReduced ? staticVariant : animatedVariant}
      className="p-6 bg-white dark:bg-zinc-900 rounded-lg shadow-lg"
    >
      <h2 className="text-xl font-medium">{title}</h2>
      {children}
    </motion.article>
  );
}

CSS Guard for Fade-In Animations

Use layered media queries to disable keyframe animations for users who request reduced motion:

/* styles.css */
@media (prefers-reduced-motion: no-preference) {
  .fade-in {
    animation: fadeIn 0.5s ease-out forwards;
  }
}

@media (prefers-reduced-motion: reduce) {
  .fade-in {
    animation: none;
  }
}

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

Scroll-Driven Animation with Static Fallback

This pattern uses useScroll and useTransform while respecting reduced-motion preferences:

import { motion, useScroll, useTransform, useReducedMotion } from "motion/react";

export function ParallaxBanner() {
  const prefersReduced = useReducedMotion();
  const { scrollYProgress } = useScroll();

  // Motion value is ignored when reduced‑motion is active
  const y = useTransform(
    scrollYProgress,
    [0, 1],
    prefersReduced ? [0, 0] : [0, -200]   // static when reduced
  );

  return (
    <motion.div style={{ y }} className="h-64 bg-cover bg-center">
      {/* background image */}
    </motion.div>
  );
}

Key Files and References

  • skills/taste-skill/SKILL.md — Central design-system specification. Lines 525–529 define the reduced-motion mandate, and lines 511–514 list forbidden animation patterns.
  • CHANGELOG.md — Records the introduction of the reduced-motion requirement.
  • Component files importing motion/react — Where useReducedMotion() is applied in practice (e.g., components/Card.tsx or equivalent).
  • Global stylesheet (e.g., styles/globals.css) — Holds CSS @media (prefers-reduced-motion) guards.

Summary

  • Taste-Skill mandates reduced-motion support for any animation where MOTION_INTENSITY is greater than 3, per SKILL.md lines 525–529.
  • Implement a React layer using useReducedMotion() from motion/react to toggle animation props at runtime.
  • Implement a CSS layer using @media (prefers-reduced-motion: reduce) to disable transitions and keyframes.
  • Avoid forbidden patterns such as window.addEventListener('scroll'); use Motion hooks like useScroll instead.
  • Test in both OS preference states and verify through Lighthouse accessibility audits.

Frequently Asked Questions

What is the MOTION_INTENSITY threshold that triggers reduced-motion support in Taste-Skill?

Any animation whose MOTION_INTENSITY exceeds 3 must unconditionally respect the user's prefers-reduced-motion setting. Motion levels 1–3 are classified as static and do not require additional reduced-motion handling, as documented in skills/taste-skill/SKILL.md line 561.

Why does Taste-Skill require both React and CSS layers for reduced motion?

The codebase contains a mix of Motion-driven JavaScript animations and pure CSS transitions. Using only one layer risks leaking motion through the other path. The useReducedMotion() hook covers React components, while @media (prefers-reduced-motion) guards cover stylesheet-based animations.

Can I use window.addEventListener('scroll') for scroll-driven animations?

No. The Taste-Skill guardrails in SKILL.md lines 511–514 explicitly forbid window.addEventListener('scroll') and manual scrollY calculations. You must rely on Motion hooks such as useScroll and useMotionValue, or use CSS scroll-timeline features.

How do I test reduced motion support during development?

Toggle your operating system's reduced-motion setting, or simulate the preference through Chrome DevTools. Verify that animated elements render in a static state. Finally, run a Lighthouse Accessibility audit to confirm the reduced-motion check passes.

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 →