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:
42 files changed
+4170
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: micro-interaction
|
||||
description: This skill should be used when the user asks to "add a hover/press effect", "animate a toggle or switch", "build an animated like button", "make a toast/snackbar slide in", "animate a drawer or modal", "animate list reordering or add/remove", "do a shared-element layout transition", or "polish UI feedback". Covers UI motion with Framer Motion (motion/react) and modern CSS.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Micro-interactions (UI Motion)
|
||||
|
||||
Small, functional motion that makes an interface feel responsive and alive: hover/press/focus feedback, toggles, toasts, drawers, and list/layout animation. The goal is feedback and continuity, not decoration.
|
||||
|
||||
## When to use
|
||||
|
||||
- Hover/press/focus feedback; toggles, checkboxes, like/heart buttons
|
||||
- Toasts/snackbars, drawers, modals, tooltips, accordions (enter/exit)
|
||||
- List add/remove/reorder; shared-element ("magic move") layout transitions
|
||||
- Loading → success → error state transitions
|
||||
|
||||
## Principles (apply to all of it)
|
||||
|
||||
- Duration: UI micro-interactions live in **100–250ms**. Anything over ~400ms feels laggy for a click response.
|
||||
- Animate `transform` and `opacity` only — they are GPU-composited (no layout/paint). Avoid animating `width/height/top/left`; use `scale` or layout animation instead.
|
||||
- Give **instant** press feedback: `scale: 0.96` on tap with a fast spring.
|
||||
- Asymmetric timing: enter slightly slower (ease-out), exit faster (ease-in). Things should arrive gracefully and leave promptly.
|
||||
- Respect `prefers-reduced-motion`: gate non-essential motion; keep opacity changes, drop large movement.
|
||||
- Easing defaults: ease-out for entrances `cubic-bezier(0.16, 1, 0.3, 1)`; a tasteful overshoot is `cubic-bezier(0.34, 1.56, 0.64, 1)`; standard move `cubic-bezier(0.4, 0, 0.2, 1)`.
|
||||
|
||||
## Framer Motion (motion/react) essentials
|
||||
|
||||
As of v11+, the package is imported as `motion/react` (the `framer-motion` name still works).
|
||||
|
||||
### Gestures and springs
|
||||
|
||||
```jsx
|
||||
import { motion } from "motion/react";
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.96 }}
|
||||
whileFocus={{ boxShadow: "0 0 0 3px rgba(59,130,246,.5)" }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
||||
/>
|
||||
```
|
||||
|
||||
Spring intuition: higher `stiffness` = faster; higher `damping` = less bounce. `stiffness: 400, damping: 30` is a snappy UI default. For visible bounce, lower damping (e.g. `damping: 12`).
|
||||
|
||||
### Enter / exit with AnimatePresence
|
||||
|
||||
Exit animations require `AnimatePresence` wrapping conditionally-rendered children, each with a stable `key`.
|
||||
|
||||
```jsx
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
key="panel"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 8 }}
|
||||
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
For toasts/lists, use `mode="popLayout"` so removed items don't hold space while exiting.
|
||||
|
||||
### Layout animation (the superpower)
|
||||
|
||||
`layout` automatically animates any layout change (position/size) using transforms — perfect for reordering, expanding cards, and grid<->list.
|
||||
|
||||
```jsx
|
||||
<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />
|
||||
```
|
||||
|
||||
Shared-element transition across components: give two elements the same `layoutId` and Framer Motion animates between them as one mounts and the other unmounts.
|
||||
|
||||
```jsx
|
||||
{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
|
||||
<AnimatePresence>
|
||||
{open && <motion.div layoutId="card" />} {/* "magic moves" from the source */}
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
Gotcha: a plain `layout` element distorts `border-radius` and text during scale. Add `layout` to direct children that should counter-scale, and prefer `borderRadius`/`boxShadow` as motion values, or use `layout="position"` to animate position only.
|
||||
|
||||
## Pure CSS path (no JS, including discrete properties)
|
||||
|
||||
Modern CSS can animate enter/exit and even `display` toggles without a library.
|
||||
|
||||
```css
|
||||
.toast {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
/* allow animating to/from display:none and from initial render */
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
.toast[hidden] { display: none; }
|
||||
|
||||
/* animate FROM these values on first render / when entering */
|
||||
@starting-style {
|
||||
.toast { opacity: 0; transform: translateY(8px); }
|
||||
}
|
||||
```
|
||||
|
||||
`@starting-style` defines the "before-open" values so an element animates in on first appearance; `transition-behavior: allow-discrete` lets `display` (and `overlay` for popovers/dialogs) participate so exit animations run before the element is removed. Baseline in Chrome/Edge/Safari; provide a no-animation fallback for older browsers.
|
||||
|
||||
Always include a reduced-motion guard:
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State, not just decoration
|
||||
|
||||
Loading → success → error should be one continuous motion (e.g. a button morphs spinner → checkmark), not a jump cut. Keep the element mounted and animate between states so the eye tracks the same object.
|
||||
|
||||
## Deliver & verify (standalone HTML)
|
||||
|
||||
> **Packaged helper** (`scripts/`): `scripts/seek-shot.sh anim.html 0 1.5 3` freezes the `?t=N` harness and screenshots each moment; `scripts/contact-sheet.sh sheet.png frame-*.png` tiles them for one-glance review. See `scripts/README.md`.
|
||||
|
||||
For a self-contained interaction demo (toggle, like button, toast, drawer) the deliverable is **one HTML file that opens directly in a browser**. Pure-CSS interactions ship as-is; for a Framer Motion demo, load React + `motion` from CDN (`esm.sh`) into one inline module — no build step. One file is the right tier; don't reach for a bundler.
|
||||
|
||||
**Output contract:**
|
||||
- One `.html` file: your markup plus either CSS transitions/`@starting-style` or an inline `<script type="module">` importing `motion` from CDN.
|
||||
- A way to land on the **resolved end state** for a screenshot — interactions are state-driven, not time-driven, so freeze the state rather than a clock.
|
||||
|
||||
**Seek harness — pin a deterministic state.** A micro-interaction's "frames" are its states (idle / hover / pressed / open). `?state=open` applies the target state on load so a screenshot captures it settled:
|
||||
|
||||
```html
|
||||
<script>
|
||||
const s = new URLSearchParams(location.search).get("state");
|
||||
if (s) document.documentElement.dataset.state = s; // CSS keys off [data-state="open"]
|
||||
// Framer Motion: set the controlled prop from `s` (e.g. const [open]=useState(s==="open"))
|
||||
// For a CSS @keyframes loop instead, freeze it: el.style.animationDelay=(-N)+"s"; el.style.animationPlayState="paused";
|
||||
window.__ready = true;
|
||||
</script>
|
||||
```
|
||||
|
||||
**Verify loop — render → set state → screenshot → check:** open each meaningful state (`?state=idle`, `?state=hover`, `?state=open`), screenshot, and check **fidelity** (press feedback reads instant, exit runs before unmount) plus **artifacts** (`layout` distorting `border-radius`/text, toast holding space after exit, FOUC, jank). Any headless tool works:
|
||||
|
||||
```bash
|
||||
npx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.png
|
||||
```
|
||||
|
||||
**Before you finish:**
|
||||
1. Opens standalone — no console errors, CDN React/`motion` (if used) resolves.
|
||||
2. The `?state=` (or controlled-prop) freeze lands a deterministic, settled state.
|
||||
3. Screenshotted across states — idle / active / open — matches the brief, no artifacts.
|
||||
4. `prefers-reduced-motion` honored — large movement dropped, opacity/feedback kept.
|
||||
5. Easing is intentional — enter ease-out, exit ease-in, durations in the 100–250ms band.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Approach |
|
||||
|------|----------|
|
||||
| Press feedback | `whileTap={{ scale: 0.96 }}` spring 400/30 |
|
||||
| Enter/exit | `AnimatePresence` + `initial/animate/exit` |
|
||||
| Reorder / resize | `layout` prop (spring) |
|
||||
| Magic move | shared `layoutId` |
|
||||
| Toast stack | `AnimatePresence mode="popLayout"` |
|
||||
| No-JS enter | `@starting-style` + transition |
|
||||
| Animate to display:none | `transition-behavior: allow-discrete` |
|
||||
| Accessibility | `prefers-reduced-motion` guard always |
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/framer-motion-recipes.md` — variants + stagger, `AnimatePresence` modes, `layout`/`layoutId` magic move, `Reorder` drag-to-sort, drag constraints, gesture composition, and `useReducedMotion`.
|
||||
- `references/css-recipes.md` — `@starting-style`, `transition-behavior: allow-discrete`, popover/dialog exit animation, keyframe spinners/toggles, `@property` for animatable custom properties, and `prefers-reduced-motion` patterns.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user