From harness-claude
Implements accessible UI animations and motion effects respecting prefers-reduced-motion, avoiding seizure triggers, with pause controls for carousels, transitions, and parallax. Reviews compliance.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Implement animations that respect user motion preferences, avoid seizure triggers, and provide pause controls
Guides accessible motion design with reduced motion alternatives, vestibular-safe animations, and WCAG-compliant adaptations using CSS media queries and JavaScript detection.
Creates purposeful interface animations for HUDs using timing tokens, spring physics, easing functions, and reduced motion support. Ensures performance, accessibility, and consistency.
Guides purposeful UI animations with adapted Disney's 12 principles, purpose tests, easing curves, duration guidelines, choreography, and reduced motion for accessibility.
Share bugs, ideas, or general feedback.
Implement animations that respect user motion preferences, avoid seizure triggers, and provide pause controls
prefers-reduced-motion. Users who enable "Reduce motion" in their OS settings have vestibular disorders or motion sensitivity. Query this preference and eliminate or simplify animations./* Default: full animations */
.card {
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.card:hover {
transform: scale(1.05);
}
/* Reduced motion: remove or simplify */
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
.card:hover {
transform: none;
}
}
/* Approach A: motion-first (add animations, remove for reduced-motion) */
.element {
animation: slide-in 0.5s ease;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: none;
}
}
/* Approach B: reduce-first (no animations by default, add for no-preference) */
.element {
animation: none;
}
@media (prefers-reduced-motion: no-preference) {
.element {
animation: slide-in 0.5s ease;
}
}
Approach B is safer — users get no animation by default.
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function animateElement(element: HTMLElement) {
if (prefersReducedMotion) {
// Instant state change, no animation
element.style.opacity = '1';
return;
}
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, easing: 'ease-in' });
}
function usePrefersReducedMotion(): boolean {
const [prefersReduced, setPrefersReduced] = useState(
() => window.matchMedia('(prefers-reduced-motion: reduce)').matches
);
useEffect(() => {
const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, []);
return prefersReduced;
}
// Usage
function AnimatedCard() {
const reduceMotion = usePrefersReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: reduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: reduceMotion ? 0 : 0.3 }}
/>
);
}
function AutoCarousel({ slides }: { slides: Slide[] }) {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
<button
onClick={() => setIsPaused(!isPaused)}
aria-label={isPaused ? 'Play slideshow' : 'Pause slideshow'}
>
{isPaused ? <PlayIcon /> : <PauseIcon />}
</button>
<Carousel autoPlay={!isPaused} slides={slides} />
</div>
);
}
Avoid flashing content. Content that flashes more than 3 times per second can trigger seizures (WCAG 2.3.1 — Three Flashes). This is a hard constraint with no workaround.
Avoid large-scale motion. Parallax effects, zooming transitions, and full-page scroll animations are common vestibular triggers. When using these, always check prefers-reduced-motion and provide a static alternative.
Keep essential animations short. Transitions that communicate state changes (loading, success, error) should be under 500ms. Long, elaborate animations add cognitive load and frustrate users who interact frequently.
Use will-change and GPU-composited properties for performance. Janky animations that drop frames are worse than no animation. Stick to transform and opacity for smooth 60fps animations.
.animated {
will-change: transform, opacity;
transition:
transform 0.2s ease,
opacity 0.2s ease;
}
WCAG requirements:
What counts as "reduced motion":
Framer Motion integration:
<motion.div
layout
transition={{
layout: { duration: prefersReduced ? 0 : 0.3 },
}}
/>
Common mistakes:
prefers-reduced-motion once at load (user can change it at runtime — use addEventListener)https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions