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 themotion/reactpackage. This hook returnstruewhen 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— WhereuseReducedMotion()is applied in practice (e.g.,components/Card.tsxor 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_INTENSITYis greater than 3, perSKILL.mdlines 525–529. - Implement a React layer using
useReducedMotion()frommotion/reactto 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 likeuseScrollinstead. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →