
Ui Animation
- 47 installs
- 61 repo stars
- Updated August 4, 2026
- joelhooks/joelclaw
Design and review UI motion with rules for easing, timing, transform/opacity-only animation, and prefers-reduced-motion accessibility.
About
Provides guidelines and examples for UI motion, easing, timing, and reduced-motion behavior. A developer uses it when designing, implementing, or reviewing CSS transitions, keyframes, or spring animations.
- Animate only transform and opacity for movement; never layout or transition:all
- Honor prefers-reduced-motion and define easing/timing defaults (200-300ms)
Ui Animation by the numbers
- 47 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,253 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joelhooks/joelclaw --skill ui-animationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 61 |
| Last updated | August 4, 2026 |
| Repository | joelhooks/joelclaw ↗ |
What it does
Design and review UI motion with rules for easing, timing, transform/opacity-only animation, and prefers-reduced-motion accessibility.
Files
UI Animation
Core rules
- Animate to clarify cause/effect or add deliberate delight.
- Keep interactions fast (200-300ms; up to 1s only for illustrative motion).
- Never animate keyboard interactions (arrow-key navigation, shortcut responses, tab/focus).
- Prefer CSS; use WAAPI or JS only when needed.
- Make animations interruptible and input-driven.
- Honor
prefers-reduced-motion(reduce or disable).
What to animate
- For movement and spatial change, animate only
transformandopacity. - For simple state feedback,
color,background-color, andopacitytransitions are acceptable. - Never animate layout properties; never use
transition: all. - Avoid
filteranimation for core interactions; if unavoidable, keep blur <= 20px. - SVG: apply transforms on a
<g>wrapper withtransform-box: fill-box; transform-origin: center. - Disable transitions during theme switches.
Spatial and sequencing
- Set
transform-originat the trigger point. - For dialogs/menus, start around
scale(0.85-0.9); avoidscale(0). - Stagger reveals <= 50ms.
Easing defaults
- Enter and transform-based hover:
cubic-bezier(0.22, 1, 0.36, 1). - Move:
cubic-bezier(0.25, 1, 0.5, 1). - Simple hover colour/background/opacity:
200ms ease. - Avoid
ease-infor UI (feels slow).
Accessibility
- If
transformis used, disable it inprefers-reduced-motion. - Disable hover transitions on touch devices via
@media (hover: hover) and (pointer: fine).
Performance
- Pause looping animations off-screen.
- Toggle
will-changeonly during heavy motion and only fortransform/opacity. - Prefer
transformover positional props in animation libraries. - Do not animate drag gestures using CSS variables.
Reference
- Snippets and practical tips: examples.md
Workflow
1. Start with the core rules, then pick a reference snippet from examples.md. 2. Keep motion functional; honor prefers-reduced-motion. 3. When reviewing, cite file paths and line numbers and propose concrete fixes.
UI Animation Examples
Snippets and tips for the core rules in SKILL.md.
Table of contents
- Enter and exit
- Spatial rules and stagger
- Drawer (move easing)
- Hover transitions
- Reduced motion
- Origin-aware animations
- Performance recipes
- Practical tips
Enter and exit
/* Toast.module.css */
.toast {
transform: translate3d(0, 6px, 0);
opacity: 0;
transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.toast[data-open="true"] {
transform: translate3d(0, 0, 0);
opacity: 1;
}
/* Disable transitions during theme switch */
[data-theme-switching="true"] * {
transition: none !important;
}// app/components/Panel.tsx
"use client";
import { motion, useReducedMotion } from "framer-motion";
export function Panel() {
const reduceMotion = useReducedMotion();
return (
<motion.div
whileHover={reduceMotion ? undefined : { scale: 1.02 }}
whileTap={reduceMotion ? undefined : { scale: 0.98 }}
transition={
reduceMotion ? { duration: 0 } : { duration: 0.2, ease: [0.22, 1, 0.36, 1] }
}
/>
);
}Spatial rules and stagger
/* Menu.module.css */
.menu {
transform-origin: top right;
transform: scale(0.88);
opacity: 0;
transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
.menu[data-open="true"] {
transform: scale(1);
opacity: 1;
}
.list > * {
animation: fade-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.list > *:nth-child(2) { animation-delay: 50ms; }
.list > *:nth-child(3) { animation-delay: 100ms; }const listVariants = {
show: { transition: { staggerChildren: 0.05 } },
};Drawer (move easing)
.drawer {
transition: transform 240ms cubic-bezier(0.25, 1, 0.5, 1);
}<motion.aside
initial={{ transform: "translate3d(100%, 0, 0)" }}
animate={{ transform: "translate3d(0, 0, 0)" }}
exit={{ transform: "translate3d(100%, 0, 0)" }}
transition={{ duration: 0.24, ease: [0.25, 1, 0.5, 1] }}
/>Hover transitions
/* Link.module.css */
@media (hover: hover) and (pointer: fine) {
.link {
transition: color 200ms ease, opacity 200ms ease;
}
.link:hover {
opacity: 0.8;
}
}Reduced motion
@media (prefers-reduced-motion: reduce) {
.menu,
.toast {
transform: none;
transition: none;
}
}"use client";
import { motion, useReducedMotion } from "framer-motion";
export function AnimatedCard() {
const reduceMotion = useReducedMotion();
return (
<motion.div
animate={reduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}
initial={reduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.98 }}
/>
);
}Origin-aware animations
.popover[data-side="top"] { transform-origin: bottom center; }
.popover[data-side="bottom"] { transform-origin: top center; }
.popover[data-side="left"] { transform-origin: center right; }
.popover[data-side="right"] { transform-origin: center left; }Performance recipes
Pause looping animations off-screen
// app/hooks/usePauseOffscreen.ts
"use client";
import { useEffect, useRef } from "react";
export function usePauseOffscreen<T extends HTMLElement>() {
const ref = useRef<T | null>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
el.style.animationPlayState = entry.isIntersecting ? "running" : "paused";
});
io.observe(el);
return () => io.disconnect();
}, []);
return ref;
}Toggle will-change during animation
.animating {
will-change: transform, opacity;
}Spring defaults (framer-motion)
<motion.div
animate={{ transform: "translate3d(0, 0, 0)" }}
transition={{ type: "spring", stiffness: 500, damping: 40 }}
/>Practical tips
Record your animations
When something feels off, record the animation and play it back frame by frame.
Fix shaky 1px shifts
Elements can shift by 1px at the start/end of CSS transforms due to GPU/CPU handoff. Apply will-change: transform during the animation (not permanently) to keep compositing on the GPU.
Scale buttons on press
button:active {
transform: scale(0.97);
opacity: 0.9;
}Avoid animating from scale(0)
.element {
transform: scale(0.95);
opacity: 0;
}
.element.visible {
transform: scale(1);
opacity: 1;
}Skip animation on subsequent tooltips
.tooltip {
transition:
transform 125ms ease-out,
opacity 125ms ease-out;
transform-origin: var(--transform-origin);
}
.tooltip[data-starting-style],
.tooltip[data-ending-style] {
opacity: 0;
transform: scale(0.97);
}
.tooltip[data-instant] {
transition-duration: 0ms;
}Fix hover flicker
Apply the hover effect on a parent, animate the child:
.box:hover .box-inner {
transform: translateY(-20%);
}
.box-inner {
transition: transform 200ms ease;
}