.agents/skills/anim/SKILL.md
# Tasteful Web Animation — Skill **Name:** `anim` **Purpose:** Tasteful, subtle web animations following Emil Kowalski's philosophy and animations.dev principles. Use this skill when adding motion to interfaces — hover states, page transitions, micro-interactions, loading states, or any UI animation — so motion stays refined and purposeful, not decorative noise. **Applies when:** Adding or reviewing UI motion (CSS, Web APIs, or React); hover and press feedback; entrances/exits; modals, toasts,
npx skillsauth add asymmetric-al/core .agents/skills/animInstall 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.
Name: anim
Purpose: Tasteful, subtle web animations following Emil Kowalski's philosophy and animations.dev principles. Use this skill when adding motion to interfaces — hover states, page transitions, micro-interactions, loading states, or any UI animation — so motion stays refined and purposeful, not decorative noise.
Applies when: Adding or reviewing UI motion (CSS, Web APIs, or React); hover and press feedback; entrances/exits; modals, toasts, menus; loading and skeleton patterns; staggered reveals; page or view transitions.
Do not use when: Motion would hurt clarity or accessibility; the task is only motion/react plumbing — pair with the motion skill for API specifics. Skip motion for validation errors, critical errors, actively read content, and high-frequency live updates.
prefers-reduced-motion or useReducedMotion); avoid layout-affecting properties and animating on every re-render.Animation should be invisible. When done right, users don't notice animation — they notice that the interface feels good. The moment someone says "nice animation," you've probably overdone it.
"The best animations are the ones you don't notice." — Emil Kowalski
Micro-interactions: 150-250ms. Hovers, button presses, toggles. Anything faster feels instant (good); anything slower feels sluggish (bad).
Standard transitions: 200-350ms. Modals opening, panels sliding, content appearing. This is your bread and butter.
Complex orchestrations: 400-600ms total. Page transitions, multi-step reveals. Never longer unless you have a very good reason.
Exit animations should be faster than entrances. Users are waiting to do something next. Enter at 300ms, exit at 200ms.
Stagger delays: 30-60ms between items. Longer staggers (100ms+) feel like a slideshow. Keep it tight.
Never animate for more than 1 second total. If your animation takes longer, it's not an animation - it's a loading screen.
Default to ease-out for entrances. Elements arriving should decelerate naturally, like a car pulling into a parking spot.
Use ease-in for exits. Elements leaving should accelerate away, like releasing a bowstring.
Use ease-in-out sparingly. Only for elements that move from point A to point B while staying on screen (dragging, repositioning).
Never use linear easing for UI. Linear is for progress bars and looping background animations only. Real objects don't move linearly.
Prefer spring physics for organic motion. Springs have natural overshoot and settle. In Motion for React, use transition={{ type: "spring", stiffness: 400, damping: 25 }} (tune as needed); in CSS, use cubic-bezier() or linear() curves that approximate a spring.
Match easing to physical metaphor. Dropping? Ease-in with bounce. Rising? Ease-out. Sliding? Ease-in-out.
Consistent easing across related elements. If a modal and its backdrop animate together, they must use the same curve.
Animate transform and opacity only (when possible). These are GPU-accelerated and won't cause layout thrashing.
Never animate width, height, top, left, margin, or padding. These trigger expensive layout recalculations. Use transform: scale() or translate() instead.
Animate from a definite state to a definite state. Never animate to/from auto or computed values without measuring first.
Scale from center for growth, from origin for menus. Dropdowns scale from their trigger. Modals scale from center. Be intentional.
Opacity changes should accompany movement. Don't just fade - fade AND move. opacity: 0 + translateY(8px) → opacity: 1 + translateY(0).
Keep movement distances small. 4-16px for micro-interactions. 20-40px for larger reveals. Anything more looks cartoony.
Hover: instant on, 150ms off. Respond immediately when hovering; ease out when leaving so it doesn't "snap" away.
Active/pressed: scale(0.97-0.98). Subtle compression. Never go below 0.95 - that's cartoon territory.
Focus: never animate the focus ring itself. Focus indicators are for accessibility. Animate the element, not the indicator.
Disabled elements: no animation. Disabled means disabled. Don't tease users with hover effects on things they can't click.
Loading states: subtle pulse or skeleton shimmer. Not spinners unless absolutely necessary. Keep the rhythm calm.
Fade + rise for content appearing. opacity: 0, y: 8 → opacity: 1, y: 0. The classic for a reason.
Fade + sink for content disappearing. Reverse is not always best. Sometimes exit down, not up, for natural gravity.
Scale for emphasis, translate for navigation. Opening something important? Scale. Moving to a new view? Slide.
Modals: scale(0.96) + opacity, not scale(0). Starting from nothing looks cheap. Start nearly there.
Toasts: slide from edge + fade. Come from where they'll return to. Slide in from right, slide out to right.
Menus: transform-origin at trigger, scale + opacity. Dropdowns should bloom from their source.
Lead with the most important element. In a stagger sequence, the primary content animates first.
Background elements animate first, foreground last. Backdrop → container → content → actions.
Use stagger for related items only. A list of cards? Stagger. Unrelated UI elements? Animate together.
Keep stagger groups small (3-7 items). More than that and the last item waits too long.
Exit in reverse order or all-at-once. Either mirror the entrance stagger (last in, first out) or don't stagger exits at all.
Always respect prefers-reduced-motion. Not optional. Wrap motion in @media (prefers-reduced-motion: no-preference) or check the query in JS.
Use will-change only when needed, remove after. Apply before animation starts, remove after it ends. Never leave it on permanently.
Avoid animating during scroll. Scroll-linked animations can jank. Use scroll-timeline or Intersection Observer sparingly.
Test on low-end devices. That buttery M3 Mac animation becomes a slideshow on a $200 Android.
Don't animate layout on mobile. Mobile browsers struggle with layout animations. Keep it to transforms and opacity.
.element {
transition:
transform 200ms ease-out,
opacity 200ms ease-out;
}
/* Hover: instant on, fade off */
.element:hover {
transform: translateY(-2px);
transition-duration: 0ms; /* instant on */
}
.element:not(:hover) {
transition-duration: 150ms; /* ease off */
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.entering {
animation: fadeInUp 250ms ease-out forwards;
}
/* Approximated spring curve */
:root {
--spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--spring-smooth: cubic-bezier(0.22, 1, 0.36, 1);
--spring-snappy: cubic-bezier(0.16, 1, 0.3, 1);
}
.item {
animation: fadeInUp 200ms ease-out backwards;
}
.item:nth-child(1) {
animation-delay: 0ms;
}
.item:nth-child(2) {
animation-delay: 40ms;
}
.item:nth-child(3) {
animation-delay: 80ms;
}
.item:nth-child(4) {
animation-delay: 120ms;
}
.item:nth-child(5) {
animation-delay: 160ms;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
/>
<motion.div
animate={{ scale: 1 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
<motion.ul
initial="hidden"
animate="visible"
variants={{
visible: { transition: { staggerChildren: 0.04 } },
}}
>
{items.map((item) => (
<motion.li
key={item.id}
variants={{
hidden: { opacity: 0, y: 8 },
visible: { opacity: 1, y: 0 },
}}
/>
))}
</motion.ul>
<AnimatePresence mode="wait">
<motion.div
key={currentView}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
/>
</AnimatePresence>
@asym/lib/motion-presets — canonical durations, easings, stagger steps, SCALE_ENTRANCE (0.96), and helpers (propsHeroEntrance, propsFadeRiseInView, propsScaleFadeInView) that respect useReducedMotion(). Prefer importing these in client components next to @asym/lib/motion or motion/react.@asym/lib/motion — re-exports useReducedMotion for LazyMotion trees."Animation is not about moving things. It's about not making users wait." — Emil Kowalski
development
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
testing
Use when CI tests fail on main branch after PR merge, or when investigating flaky test failures in CI environments
tools
Use when new translation keys are added to packages to generate new translations strings
data-ai
Pointer to the canonical agent instruction and skill system for this monorepo