Skip to content
Frontend
Skill

/accessible-animation

This skill should be used when the user asks to "respect prefers-reduced-motion", "honor reduced motion", "make my animations accessible", "fix vestibular / motion-sickness issues", "add a useReducedMotion hook", "gate GSAP / Framer Motion / Lenis for reduced motion", or "meet

BOOST
From plugin
web-animation-skills
459 skills
Install
$ npx -y skills add iart-ai/web-animation-skills --skill accessible-animation --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/accessible-animation

Context preview

The summary Claude sees to decide when to auto-load this skill.

This skill should be used when the user asks to "respect prefers-reduced-motion", "honor reduced motion", "make my animations accessible", "fix vestibular / motion-sickness issues", "add a useReducedMotion hook", "gate GSAP / Framer Motion / Lenis for reduced motion", or "meet

SKILL.md

accessible-animation.SKILL.md
name: accessible-animation
description: This skill should be used when the user asks to "respect prefers-reduced-motion", "honor reduced motion", "make my animations accessible", "fix vestibular / motion-sickness issues", "add a useReducedMotion hook", "gate GSAP / Framer Motion / Lenis for reduced motion", or "meet WCAG 2.3.3 / C39". Provides tiered (not all-or-nothing) reduced-motion patterns in CSS and JS.
version: 0.1.0

Accessible Animation (Reduced Motion, Tiered)

Honor `prefers-reduced-motion: reduce` as a *graceful degradation*, not a kill switch. The OS-level setting (macOS: Settings > Accessibility > Display > Reduce motion; Windows: Settings > Accessibility > Visual effects > Animation effects; iOS/Android equivalents) signals that *vestibular-triggering* motion causes nausea, dizziness, or migraines. The correct response is to remove the dangerous motion while keeping orientation cues.

When to use

Use when adding or auditing animation in a web UI, when a user reports motion sickness or WCAG findings, or when integrating animation libraries (GSAP, Framer Motion, Lenis smooth scroll, Anime.js, CSS keyframes) into an accessible product.

The core principle: tier motion, do not nuke it

Removing *all* animation is a common over-correction. An instant state change with no transition can be more disorienting (elements teleport) and removes useful affordances (focus rings, loading spinners that communicate progress). Sort every animation into three tiers:

  • **Tier 1 — Remove entirely (vestibular triggers).** Parallax; large-area slides/translates across the viewport; scale/zoom of large elements; 3D rotation and spin; continuous auto-playing carousels and marquees; smooth-scroll hijacking (Lenis/Locomotive); scroll-jacked pinned scenes; motion-path animation; anything that moves a large portion of the screen or implies depth/self-motion.
  • **Tier 2 — Soften / shorten.** Replace movement with a short opacity fade (≤200ms). Reduce distance and duration. Keep the *fact* of a transition (so elements don't teleport) but strip the displacement. Replace a 600ms slide-in-from-left with a 150ms cross-fade.
  • **Tier 3 — Always keep.** Opacity fades, color/background transitions, focus-ring transitions, loading indicators, and motion that is *essential* to meaning (WCAG allows essential animation). These rarely trigger vestibular responses because they imply no self-motion.

CSS layer: media-query gating

Write motion as the default, then override under the reduce query. Prefer a global safety net plus targeted overrides.

/* Global safety net: neutralize runaway motion but DO NOT set 0s blindly,
   which can break JS that waits for transitionend/animationend. Use 0.01ms. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;   /* stop infinite spins/marquees */
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;          /* kill smooth scroll */
  }
}

The global net is a backstop. Layer *intentional, tiered* overrides on top so Tier 3 motion survives:

.card {
  transition: transform 400ms ease, opacity 400ms ease, background-color 200ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    /* Tier 2: drop the transform (movement), keep opacity + color (Tier 3) */
    transition: opacity 150ms ease, background-color 150ms ease;
  }
  .parallax-layer { transform: none !important; }        /* Tier 1: remove */
  .hero-zoom      { animation: none !important; }        /* Tier 1: remove */
}

Use the *positive* query `(prefers-reduced-motion: no-preference)` to opt motion IN, which is the safest pattern for elaborate effects:

.hero { opacity: 1; } /* visible, static by default */
@media (prefers-reduced-motion: no-preference) {
  .hero { animation: zoom-in 1.2s ease both; } /* only animate when allowed */
}

JS layer: matchMedia gating

CSS cannot gate JS-driven animation (GSAP timelines, Framer's `animate`, canvas/WebGL, Lenis). Read the same signal via `matchMedia` and react to live changes (users toggle the OS setting without reloading).

const REDUCE_QUERY = '(prefers-reduced-motion: reduce)';

export function prefersReducedMotion() {
  return typeof window !== 'undefined'
    && window.matchMedia
    && window.matchMedia(REDUCE_QUERY).matches;
}

// Live updates: listen so toggling the OS setting takes effect immediately.
export function onReducedMotionChange(cb) {
  const mq = window.matchMedia(REDUCE_QUERY);
  const handler = (e) => cb(e.matches);
  mq.addEventListener('change', handler);          // modern API
  return () => mq.removeEventListener('change', handler);
}

Library gating, applied as one reusable layer

// GSAP — use matchMedia(): GSAP reverts conditional setups on change.
import gsap from 'gsap';
const mm = gsap.matchMedia();
mm.add({
  motionOK: '(prefers-reduced-motion: no-preference)',
  motionReduce: '(prefers-reduced-motion: reduce)',
}, (ctx) => {
  const { motionOK } = ctx.conditions;
  if (motionOK) {
    gsap.from('.hero', { y: 80, opacity: 0, duration: 1 });   // Tier 1 move
  } else {
    gsap.from('.hero', { opacity: 0, duration: 0.15 });        // Tier 2 fade
  }
});

// Lenis smooth scroll — do not instantiate at all when reduced.
import Lenis from 'lenis';
let lenis = null;
if (!prefersReducedMotion()) {
  lenis = new Lenis();
  const raf = (t) => { lenis.raf(t); requestAnimationFrame(raf); };
  requestAnimationFrame(raf);
}

React: a `useReducedMotion` hook

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const mq = window.matchMedia(q
Read more
Ships withweb-animation-skills

Web animation skills for AI coding agents — GSAP, SVG, Lottie, micro-interactions, page transitions, and 60fps performance, installed straight into your agent.

Get the whole plugin

Other skills on web-animation-skills.