Add web animation skills (iart-ai/web-animation-skills)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeokZtSmqg42ZGWsLXX2C
This commit is contained in:
Claude committed 2026-10-18 21:51:39 +00:00
1 parent 0b42fdeae1
commit 59c7859bd4
42 files changed
+4170

No files matched your search

@@ -0,0 +1,142 @@
# CSS Micro-interaction Recipes
Modern CSS handles most UI motion with zero JavaScript, including enter/exit and `display` toggles. Baseline-available in current Chrome/Edge/Safari/Firefox (verify `@starting-style` and `allow-discrete` for your support floor; degrade to instant show/hide otherwise).
## Enter animation on first render: @starting-style
`@starting-style` supplies the values an element transitions FROM the first time it renders (or becomes shown). Without it, there is no "previous" state to transition from, so newly-inserted elements snap in.
```css
.menu {
opacity: 1;
transform: translateY(0);
transition: opacity 0.2s ease, transform 0.2s ease;
}
@starting-style {
.menu { opacity: 0; transform: translateY(-8px); }
}
```
## Animating to/from display:none: transition-behavior: allow-discrete
`display` and `content-visibility` are discrete properties — normally they flip instantly, killing exit animations. `allow-discrete` lets them flip at the correct moment (kept visible during exit, swapped at the end).
```css
.drawer {
transition:
transform 0.25s ease,
opacity 0.25s ease,
display 0.25s allow-discrete; /* keep displayed through the exit */
}
.drawer[hidden] { /* closed state */
opacity: 0;
transform: translateX(100%);
display: none;
}
@starting-style {
.drawer:not([hidden]) { opacity: 0; transform: translateX(100%); }
}
```
Shorthand alternative: `transition-behavior: allow-discrete;` applies to all discrete properties in the `transition` list.
## Popover / dialog exit animation (top-layer)
Native `popover` and `<dialog>` live in the top layer via the `overlay` property, which is also discrete — animate it too:
```css
[popover] {
opacity: 0;
transform: scale(0.95);
transition: opacity 0.2s, transform 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}
[popover]:popover-open { opacity: 1; transform: scale(1); }
@starting-style {
[popover]:popover-open { opacity: 0; transform: scale(0.95); }
}
```
This gives a fully CSS-driven open AND close animation for `<button popovertarget>` popovers with no JS.
## Press / hover / focus feedback
```css
.btn {
transition: transform 0.12s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.15); }
.btn:active { transform: translateY(0) scale(0.97); transition-duration: 0.06s; } /* snappy press */
.btn:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
```
Use `:focus-visible` (not `:focus`) so keyboard users get a ring but mouse clicks don't.
## Animated toggle / switch
```css
.switch { width: 44px; height: 24px; border-radius: 999px; background: #cbd5e1;
transition: background 0.2s ease; }
.switch[aria-checked="true"] { background: #22c55e; }
.switch::after {
content: ""; width: 20px; height: 20px; border-radius: 50%; background: #fff;
transform: translateX(2px);
transition: transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.switch[aria-checked="true"]::after { transform: translateX(22px); }
```
The `cubic-bezier(0.34, 1.56, 0.64, 1)` overshoot gives the knob a satisfying snap.
## Loading spinner and pulse
```css
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { animation: spin 0.8s linear infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.skeleton { animation: pulse 1.5s ease-in-out infinite; }
```
## Animatable custom properties with @property
Plain CSS variables animate discretely. Register them with `@property` to give them a type so they interpolate smoothly — enables gradient angle, number counters, and themed motion.
```css
@property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.gradient-border {
background: conic-gradient(from var(--angle), #6366f1, #ec4899, #6366f1);
transition: --angle 0.4s ease;
}
.gradient-border:hover { --angle: 360deg; }
```
## Accordion height (modern: interpolate-size)
Animating `height: auto` historically required JS measurement. With `interpolate-size: allow-keywords`, transitions to/from intrinsic sizes work:
```css
:root { interpolate-size: allow-keywords; }
.accordion__panel {
height: 0; overflow: hidden;
transition: height 0.3s ease;
}
.accordion__panel[data-open] { height: auto; }
```
Fallback for unsupported browsers: animate `grid-template-rows: 0fr -> 1fr` on a wrapper.
## Reduced motion (always include)
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
Then selectively re-enable essential, low-motion feedback (e.g. opacity-only fades) inside the same query if needed. The principle: never remove feedback entirely, just remove large/spatial motion.
@@ -0,0 +1,187 @@
# Framer Motion Recipes
Import from `motion/react` (v11+). All examples are React + Framer Motion.
```jsx
import { motion, AnimatePresence, Reorder, useReducedMotion, useDragControls } from "motion/react";
```
## Variants and staggered children
Variants are named animation states a parent can propagate to children, enabling orchestration like staggering without per-child timing math.
```jsx
const list = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.06, delayChildren: 0.1 },
},
};
const item = {
hidden: { opacity: 0, y: 12 },
show: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 500, damping: 30 } },
};
<motion.ul variants={list} initial="hidden" animate="show">
{items.map((t) => (
<motion.li key={t.id} variants={item}>{t.label}</motion.li>
))}
</motion.ul>
```
`staggerDirection: -1` staggers from last to first. `when: "beforeChildren"` / `"afterChildren"` sequences parent vs children.
## AnimatePresence modes
```jsx
<AnimatePresence mode="wait"> {/* outgoing finishes before incoming starts (route/tab switches) */}
<motion.div key={tab} initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}} />
</AnimatePresence>
```
Modes:
- default (`"sync"`) — incoming and outgoing animate simultaneously.
- `"wait"` — finish exit before enter (single-element swaps like tabs/pages).
- `"popLayout"` — exiting items are popped from layout flow so siblings re-flow immediately (toast stacks, removing list items). Children must be capable of `layout`.
Require a unique, stable `key` on each child. To run exit on initial mount too, that is not needed; to suppress enter animation on first render set `<AnimatePresence initial={false}>`.
## Toast stack (popLayout + layout)
```jsx
function Toasts({ toasts }) {
return (
<div className="toast-region">
<AnimatePresence mode="popLayout">
{toasts.map((t) => (
<motion.div
key={t.id}
layout
initial={{ opacity: 0, y: 24, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.15 } }}
transition={{ type: "spring", stiffness: 500, damping: 40 }}
>
{t.message}
</motion.div>
))}
</AnimatePresence>
</div>
);
}
```
`layout` makes remaining toasts slide up smoothly when one is dismissed.
## Magic-move / shared-element layout
```jsx
function Gallery() {
const [active, setActive] = useState(null);
return (
<>
<div className="grid">
{photos.map((p) => (
<motion.img key={p.id} layoutId={`photo-${p.id}`} src={p.thumb}
onClick={() => setActive(p)} />
))}
</div>
<AnimatePresence>
{active && (
<motion.div className="backdrop" onClick={() => setActive(null)}
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
<motion.img layoutId={`photo-${active.id}`} src={active.full} />
</motion.div>
)}
</AnimatePresence>
</>
);
}
```
Matching `layoutId` makes the thumbnail visually fly to the lightbox and back.
## Drag-to-reorder list
`Reorder` handles pointer drag, axis locking, and live reordering with layout animation built in.
```jsx
function SortableList({ items, setItems }) {
return (
<Reorder.Group axis="y" values={items} onReorder={setItems}>
{items.map((item) => (
<Reorder.Item key={item} value={item}
whileDrag={{ scale: 1.03, boxShadow: "0 8px 24px rgba(0,0,0,.18)" }}>
{item}
</Reorder.Item>
))}
</Reorder.Group>
);
}
```
For a dedicated drag handle, use `useDragControls`:
```jsx
const controls = useDragControls();
<Reorder.Item value={item} dragListener={false} dragControls={controls}>
<span className="handle" onPointerDown={(e) => controls.start(e)}>⠿</span>
{item.label}
</Reorder.Item>
```
## Drag with constraints and elastic
```jsx
const ref = useRef(null);
<div ref={ref} className="bounds">
<motion.div drag dragConstraints={ref} dragElastic={0.2}
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }} />
</div>
```
`dragElastic` (0–1) controls how far past constraints it can be pulled; `dragSnapToOrigin` returns it on release.
## Gesture composition (like button)
```jsx
function Like() {
const [liked, setLiked] = useState(false);
return (
<motion.button onClick={() => setLiked((v) => !v)} whileTap={{ scale: 0.85 }}>
<motion.span
animate={liked ? { scale: [1, 1.4, 1], color: "#ef4444" } : { scale: 1, color: "#9ca3af" }}
transition={{ duration: 0.35, ease: "easeOut" }}
>♥</motion.span>
</motion.button>
);
}
```
Arrays of values (`scale: [1, 1.4, 1]`) are keyframes; control their spacing with `times: [0, 0.6, 1]`.
## Respecting reduced motion
```jsx
function Panel() {
const reduce = useReducedMotion();
return (
<motion.div
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
/>
);
}
```
Wrap an app in `<MotionConfig reducedMotion="user">` to globally strip transform/layout animations for users who prefer reduced motion while keeping opacity.
## Performance notes
- Prefer `transform`/`opacity`; Framer Motion already uses transforms for `x/y/scale/rotate`.
- For frequently-updated values (cursor follow, scroll), use `useMotionValue` + `useSpring` instead of React state to avoid re-renders.
- `layout` is powerful but measure-heavy; avoid putting it on hundreds of simultaneously-animating nodes.
---
Tune the timing and easing and a micro-interaction feels effortless. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=web-animation-skills&utm_content=skill_footer&utm_term=micro-interaction)** — the AI motion agent for editable, on-brand motion graphics.