Skip to content

Instantly share code, notes, and snippets.

@corysimmons
Created January 21, 2026 03:48
Show Gist options
  • Select an option

  • Save corysimmons/1e2f64603ae234602f92dafe2b549ea9 to your computer and use it in GitHub Desktop.

Select an option

Save corysimmons/1e2f64603ae234602f92dafe2b549ea9 to your computer and use it in GitHub Desktop.
Put in ~/your-project/.claude/skills/emil-anim/SKILL.md then tell Claude Code "use the emil-anim skill to audit and improve my frontend"
name description
anim
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. Produces refined, purposeful motion that enhances UX without becoming decorative noise.

Tasteful Web Animation

This skill implements refined, purposeful animations inspired by Emil Kowalski's work and the principles from animations.dev. The goal is motion that feels inevitable and natural - not decorative or attention-seeking.

Core Philosophy

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

The 40 Rules of Tasteful Animation

Timing & Duration

  1. Micro-interactions: 150-250ms. Hovers, button presses, toggles. Anything faster feels instant (good); anything slower feels sluggish (bad).

  2. Standard transitions: 200-350ms. Modals opening, panels sliding, content appearing. This is your bread and butter.

  3. Complex orchestrations: 400-600ms total. Page transitions, multi-step reveals. Never longer unless you have a very good reason.

  4. Exit animations should be faster than entrances. Users are waiting to do something next. Enter at 300ms, exit at 200ms.

  5. Stagger delays: 30-60ms between items. Longer staggers (100ms+) feel like a slideshow. Keep it tight.

  6. Never animate for more than 1 second total. If your animation takes longer, it's not an animation - it's a loading screen.

Easing & Physics

  1. Default to ease-out for entrances. Elements arriving should decelerate naturally, like a car pulling into a parking spot.

  2. Use ease-in for exits. Elements leaving should accelerate away, like releasing a bowstring.

  3. Use ease-in-out sparingly. Only for elements that move from point A to point B while staying on screen (dragging, repositioning).

  4. Never use linear easing for UI. Linear is for progress bars and looping background animations only. Real objects don't move linearly.

  5. Prefer spring physics for organic motion. Springs have natural overshoot and settle. Use transition: spring(1, 80, 10) in Motion or CSS linear() approximations.

  6. Match easing to physical metaphor. Dropping? Ease-in with bounce. Rising? Ease-out. Sliding? Ease-in-out.

  7. Consistent easing across related elements. If a modal and its backdrop animate together, they must use the same curve.

What to Animate

  1. Animate transform and opacity only (when possible). These are GPU-accelerated and won't cause layout thrashing.

  2. Never animate width, height, top, left, margin, or padding. These trigger expensive layout recalculations. Use transform: scale() or translate() instead.

  3. Animate from a definite state to a definite state. Never animate to/from auto or computed values without measuring first.

  4. Scale from center for growth, from origin for menus. Dropdowns scale from their trigger. Modals scale from center. Be intentional.

  5. Opacity changes should accompany movement. Don't just fade - fade AND move. opacity: 0 + translateY(8px)opacity: 1 + translateY(0).

  6. Keep movement distances small. 4-16px for micro-interactions. 20-40px for larger reveals. Anything more looks cartoony.

Interaction States

  1. Hover: instant on, 150ms off. Respond immediately when hovering; ease out when leaving so it doesn't "snap" away.

  2. Active/pressed: scale(0.97-0.98). Subtle compression. Never go below 0.95 - that's cartoon territory.

  3. Focus: never animate the focus ring itself. Focus indicators are for accessibility. Animate the element, not the indicator.

  4. Disabled elements: no animation. Disabled means disabled. Don't tease users with hover effects on things they can't click.

  5. Loading states: subtle pulse or skeleton shimmer. Not spinners unless absolutely necessary. Keep the rhythm calm.

Entrance & Exit Patterns

  1. Fade + rise for content appearing. opacity: 0, y: 8opacity: 1, y: 0. The classic for a reason.

  2. Fade + sink for content disappearing. Reverse is not always best. Sometimes exit down, not up, for natural gravity.

  3. Scale for emphasis, translate for navigation. Opening something important? Scale. Moving to a new view? Slide.

  4. Modals: scale(0.96) + opacity, not scale(0). Starting from nothing looks cheap. Start nearly there.

  5. Toasts: slide from edge + fade. Come from where they'll return to. Slide in from right, slide out to right.

  6. Menus: transform-origin at trigger, scale + opacity. Dropdowns should bloom from their source.

Orchestration & Staggering

  1. Lead with the most important element. In a stagger sequence, the primary content animates first.

  2. Background elements animate first, foreground last. Backdrop → container → content → actions.

  3. Use stagger for related items only. A list of cards? Stagger. Unrelated UI elements? Animate together.

  4. Keep stagger groups small (3-7 items). More than that and the last item waits too long.

  5. Exit in reverse order or all-at-once. Either mirror the entrance stagger (last in, first out) or don't stagger exits at all.

Performance & Accessibility

  1. Always respect prefers-reduced-motion. Not optional. Wrap motion in @media (prefers-reduced-motion: no-preference) or check the query in JS.

  2. Use will-change only when needed, remove after. Apply before animation starts, remove after it ends. Never leave it on permanently.

  3. Avoid animating during scroll. Scroll-linked animations can jank. Use scroll-timeline or Intersection Observer sparingly.

  4. Test on low-end devices. That buttery M3 Mac animation becomes a slideshow on a $200 Android.

  5. Don't animate layout on mobile. Mobile browsers struggle with layout animations. Keep it to transforms and opacity.

CSS Implementation Patterns

Standard Transition Setup

.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 */
}

Fade + Rise Entrance

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.entering {
  animation: fadeInUp 250ms ease-out forwards;
}

Spring-like Easing (CSS)

/* 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);
}

Stagger Pattern

.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; }

Reduced Motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Motion (Framer Motion) Patterns

Fade + Rise

<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] }}
/>

Spring Physics

<motion.div
  animate={{ scale: 1 }}
  whileTap={{ scale: 0.97 }}
  transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>

Stagger Children

<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>

Exit Before Enter (AnimatePresence)

<AnimatePresence mode="wait">
  <motion.div
    key={currentView}
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    exit={{ opacity: 0 }}
    transition={{ duration: 0.15 }}
  />
</AnimatePresence>

Common Mistakes to Avoid

  • Bouncy everything. Bounce is for celebration (confetti, success). Not for opening menus.
  • Slow fades. If opacity takes more than 200ms, it feels like lag, not elegance.
  • Scale(0) to scale(1). Looks like things popping into existence from nothing. Start at 0.95+.
  • Inconsistent directions. If modals enter from bottom, they exit to bottom. Pick a direction and commit.
  • Animating on mount unconditionally. First page load? Maybe. Every re-render? Definitely not.
  • Forgetting exit animations. Things snapping away is jarring. Every entrance needs an exit strategy.
  • Using animation to hide slow code. If you're animating to mask loading, fix the loading instead.
  • Too many things moving at once. One focal animation, everything else is secondary or static.

When NOT to Animate

  • Form validation errors (use color/icon changes instead)
  • Critical error states (don't delay bad news)
  • Content the user is actively reading
  • High-frequency updates (live data, timers)
  • Anything the user will see hundreds of times per session

Testing Checklist

  • Does it feel good at 2x speed? (If not, it's too slow)
  • Does it feel good at 0.5x speed? (If not, it's too fast or lacks easing)
  • Does it work with reduced motion enabled?
  • Does the exit feel as considered as the entrance?
  • Would a user notice if you removed it? (If yes, reconsider)
  • Does it work on a $200 Android phone?

"Animation is not about moving things. It's about not making users wait." — Emil Kowalski

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment