.claude/skills/workflow/animation-safe/SKILL.md
Audit animations and transitions for motion accessibility, performance safety, and design intent. Enforces prefers-reduced-motion compliance and blocks layout-triggering transitions.
npx skillsauth add andrem-sec/psc-comet animation-safeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit animations before they ship. Motion that ignores accessibility preferences, triggers layout recalculation, or exists purely as decoration creates real problems — vestibular disorders, janky 60fps misses, and UI that feels busy rather than intentional.
Claude adds animations because they look good in the moment. transition: all 0.3s ease gets added to every interactive element. Scroll-triggered fade-ins appear on every section. CSS keyframes loop indefinitely. None of it is gated on prefers-reduced-motion. None of it distinguishes compositor-safe properties from layout-triggering ones.
The second failure: Claude writes transition: all because it is the shortest transition declaration. transition: all transitions every CSS property simultaneously — including layout properties like width, height, padding, margin. This forces layout recalculation on every frame, destroying performance on lower-end hardware and causing visual glitches when other properties change.
The W3C defines two motion-sensitive accessibility needs:
Vestibular disorders — large-scale motion (parallax, zoom, full-screen transitions) can trigger nausea or disorientation. Approximately 35% of adults over 40 have vestibular dysfunction.
Attention/cognitive sensitivity — looping, blinking, or auto-playing animations can make a page unusable for users with attention or sensory processing differences.
The CSS media query prefers-reduced-motion: reduce is set by users in their OS accessibility settings (macOS: Settings → Accessibility → Display → Reduce Motion; Windows: Settings → Ease of Access → Display → Show animations). When set, it signals that the user wants minimal motion.
This is not optional. WCAG 2.3.3 (AAA) requires that motion triggered by interaction can be disabled. Practically, all non-essential animation should respond to this preference.
Every animation block must have a prefers-reduced-motion counterpart:
/* Allowed: transition on compositor-safe properties */
.button {
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
/* Required: reduced motion override */
@media (prefers-reduced-motion: reduce) {
.button {
transition: none;
}
}
For scroll-triggered animations:
.section {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.section.visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.section {
opacity: 1;
transform: none;
transition: none;
}
}
For JavaScript-driven animation:
// Check before animating
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) {
element.animate([...], { duration: 400 });
}
The browser rendering pipeline has four stages: JavaScript → Style → Layout → Paint → Composite. Animating properties that trigger Layout or Paint forces the browser to redo expensive work on every frame.
Safe to animate (compositor only — GPU-accelerated):
transform — translate, rotate, scale, skewopacityfilter (with GPU support)will-change: transform (use sparingly — forces GPU layer)Risky to animate (triggers Paint):
color, background-color, border-color, box-shadowNever animate (triggers Layout — jank guaranteed):
width, height, min/max-width/heightpadding, margintop, left, right, bottom (use transform: translate() instead)font-sizeborder-widthThe transition: all trap:
transition: all animates every property including Layout-triggering ones. If anything else in the component changes (class added, content updates, sibling resizes), every property transitions. This is the most common animation performance bug.
List every animation in scope:
# CSS transitions
grep -r "transition:" --include="*.css" --include="*.tsx" --include="*.jsx"
# CSS keyframes
grep -r "@keyframes\|animation:" --include="*.css"
# JavaScript animation APIs
grep -r "\.animate(\|requestAnimationFrame\|setTimeout.*style\|setInterval.*style" --include="*.js" --include="*.ts" --include="*.tsx"
# GSAP or animation libraries
grep -r "gsap\.\|TweenLite\|TweenMax\|framer-motion\|motion\." --include="*.tsx" --include="*.jsx"
For each animation found, record:
For every animation and transition found:
@media (prefers-reduced-motion: reduce) block that covers itmatchMedia('(prefers-reduced-motion: reduce)') guardBLOCK: Animation exists with no reduced-motion handling. WARN: Animation exists with partial reduced-motion handling (some elements covered, not all). CLEAN: All animations have reduced-motion fallbacks.
Classify each animated property against the safe/risky/never table above:
BLOCK — Layout-triggering:
transition: all — replace with specific propertiestransition: width, transition: height, transition: padding — replace with transform-based alternativesWARN — Paint-triggering on sustained animations:
animation: ... box-shadow in a looping keyframe — flag for reviewtransition: background-color with very short duration (<100ms) on scroll events — flag for debounceCLEAN: Only transform/opacity/filter in transitions and keyframes.
Flag gratuitous animation — animation that adds motion without adding meaning:
transition: all 0.3s ease on everything means no animation has more weight than any other. Intentional animation uses varied timing — quick for feedback (0.15s), medium for transitions (0.3s), slow for emphasis (0.5s+).## Animation Safe Report: [scope]
Date: [date]
### Accessibility (prefers-reduced-motion)
Verdict: BLOCK | WARN | CLEAN
Findings:
- Button.tsx:24 — transition: background-color 0.2s ease — no reduced-motion fallback [BLOCK]
- hero-section.css:45 — @keyframes fadeInUp — @media reduced motion block missing [BLOCK]
### Performance
Verdict: BLOCK | WARN | CLEAN
Findings:
- Card.tsx:12 — transition: all 0.3s ease — replace with: transition: box-shadow 0.2s ease, transform 0.2s ease [BLOCK]
- NavLink.tsx:8 — transition: color 0.15s ease — paint-triggered but low risk for hover state [WARN]
### Design Intent
Verdict: BLOCK | WARN | CLEAN
Findings:
- 12 of 14 sections have scroll-triggered fade-in — oversaturation [WARN]
- .spinner — looping animation appropriate for loading state [CLEAN]
### Remediations (prioritized)
1. Add @media (prefers-reduced-motion: reduce) wrapper to all transitions — accessibility requirement
2. Replace transition: all with specific properties — performance
3. Reduce scroll animations to hero and first 2 feature cards only — design intent
Replace transition: all:
/* Before */
.card { transition: all 0.3s ease; }
/* After — specify only properties that should animate */
.card { transition: box-shadow 0.2s ease, transform 0.15s ease; }
Add reduced-motion block:
/* After every animation block */
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.hero-text { animation: none; opacity: 1; transform: none; }
}
Replace layout-triggering position animation:
/* Before — triggers layout */
.slide-in { transition: left 0.3s ease; left: -100%; }
.slide-in.visible { left: 0; }
/* After — compositor only */
.slide-in { transition: transform 0.3s ease; transform: translateX(-100%); }
.slide-in.visible { transform: translateX(0); }
Do not add will-change: transform to every animated element. It consumes GPU memory and can degrade performance when overused. Only apply to elements that are actively animating.
Do not use @keyframes for hover state transitions. CSS transition handles hover states. Keyframes are for multi-step animations (loading spinners, entrance sequences).
Do not gate prefers-reduced-motion check only on the animation trigger — gate it on the CSS declaration. A user who loads the page with reduced motion active should never have the animation run, even on the first trigger.
Do not skip the performance check for mobile. transition: all is acceptable on a desktop browser. On a mid-range Android device rendering a 60fps scroll, it causes visible jank.
transition: all exists in component files (BLOCK)data-ai
Parallel agent swarm — decomposes work into independent units, spawns isolated workers, tracks PRs via fan-in
testing
Test specifically for AI-introduced regressions that repeat without tests
development
Framework for decomposing agent-driven tasks into independently verifiable units
development
Framework for designing quality agents with proper action space and contracts