PageWeave/Design

Skills / Animation & Motion

6 Skill skillanimationmotioncssspringsgsapscrolleasingmicro-interaction .md

Animation & Motion

Purposeful motion that communicates — not decorates

Install as SKILL.md

curl -o skills/animation-motion/SKILL.md /skills/animation-motion.md

Build purposeful UI animation — motion that communicates state, guides attention, and makes interfaces feel alive without feeling slow.

The Decision Framework

Before writing any animation code, answer these in order:

  1. Should this animate? Not everything should. If the animation doesn't serve feedback, orientation, focus, or continuity — skip it.
  2. What is the purpose? Feedback (button press), orientation (page transition), focus (draw eye to change), continuity (element morphs between states).
  3. What easing? Custom curves only. CSS ease is too weak. Use cubic-bezier(0.4, 0, 0.2, 1) for exits, cubic-bezier(0, 0, 0.2, 1) for entrances. Never ease-in for UI — it starts slow, feels sluggish.
  4. How fast? Under 300ms for micro-interactions. 150ms for hovers. 200–250ms for dropdowns and modals. 300–500ms for page transitions. Faster feels more responsive even when load time is identical.

Spring Physics

Springs feel more natural than duration-based animations because they simulate real physics. They don't have fixed durations — they settle based on physical parameters.

  • Stiffness: higher = snappier (100–300 for UI).
  • Damping: higher = less bounce (15–25 for UI).
  • Bounce: keep subtle (0.1–0.3). Avoid in most UI contexts. Reserve for drag-to-dismiss and playful interactions.
  • Interruptibility advantage: springs maintain velocity when interrupted — CSS animations restart from zero. Ideal for gestures users might change mid-motion.

Use Motion (Framer Motion) for React: useSpring interpolates value changes with spring-like behavior instead of updating immediately.

What to Animate

Safe properties (GPU-composited, no layout thrash):

  • transform (translate, scale, rotate)
  • opacity
  • filter (blur, brightness — use sparingly)
  • clip-path (one of the most underrated animation properties)
  • box-shadow (for elevation changes)

Never animate unless truly necessary:

  • width, height, top, left, right, bottom
  • margin, padding
  • border-width
  • Any property that triggers layout recalculation

Enter Animations

Use @starting-style for enter states:

.dialog {
  opacity: 0;
  transform: scale(0.95) translateY(8px);
  transition: opacity 200ms, transform 200ms cubic-bezier(0, 0, 0.2, 1);
}
@starting-style {
  .dialog { opacity: 0; transform: scale(0.95) translateY(8px); }
}
.dialog[open] {
  opacity: 1;
  transform: scale(1) translateY(0);
}

Never animate from scale(0) — it creates a flash. Start from scale(0.95) or scale(0.98).

Make popovers origin-aware: a popover opening from a button should transform from that button's position, not from center screen.

Stagger Animations

Staggering items within one list is legitimate. The tell is the uniform reflex — one identical entrance applied to every section. Each reveal should fit what it reveals. Vary timing, distance, and direction based on content role.

Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition — transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank.

Scroll-Triggered Motion

Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth.

For scroll-linked effects:

  • Use IntersectionObserver for triggering, not scroll position math.
  • animation-timeline: scroll() for CSS-only scroll-driven animations (modern browsers).
  • Keep scroll animations subtle — parallax of 20–40px, not 200px.

Performance Rules

  • CSS animations beat JS under load. Prefer CSS when the animation is declarative.
  • Use WAAPI (Web Animations API) for programmatic CSS animations — same performance as CSS keyframes, JS control.
  • Hardware acceleration caveat: will-change promotes to compositor layer but costs memory. Use for active animations, remove after.
  • prefers-reduced-motion is not optional. Every animation needs an alternative: typically a crossfade or instant transition.
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Component-Specific Patterns

  • Dropdowns: 150ms, ease-out-quart. Origin from trigger button.
  • Modals: 200ms, scale 0.95→1 + opacity. Backdrop fades separately at 150ms.
  • Toasts: slide in from edge (translateX), 250ms spring. Exit same direction, 200ms.
  • Page transitions: fade + subtle vertical shift (8px). 300ms max.
  • Hover states: 100–150ms. Fast enough to feel instant, slow enough to not flash.
  • Loading spinners: faster spin = feels faster load. 800ms rotation feels quicker than 1200ms.

Anti-Patterns

  • ease-in for UI animations (feels sluggish).
  • Animating from scale(0) (visual flash).
  • Bounce/elastic easing in production UI (save for playful contexts).
  • Animating layout properties without will-change or containment.
  • Uniform stagger intervals for heterogeneous content.
  • Gating content visibility on reveal classes (blank page on headless render).
  • Arbitrary values like transition: all 0.3s — specify properties explicitly.
  • Ignoring prefers-reduced-motion.

Verification

  1. Toggle prefers-reduced-motion in OS settings — all animations degrade gracefully.
  2. Tab through the interface — focus transitions should be visible but not distracting.
  3. Rapidly click/trigger the animation multiple times — it should interrupt cleanly, not queue.
  4. Profile in DevTools: no layout thrashing (purple bars in Performance tab).
  5. Test on a slow device: animations should still hit 60fps or degrade to simpler alternatives.