
Animate
- 2.3k installs
- 75 repo stars
- Updated January 28, 2026
- delphi-ai/animate-skill
animate is an agent skill for animation patterns and best practices for next.js/react applications. use this skill when implementing animations, transitions, hover effects, page transitions, modals, or any.
About
The animate skill is designed for animation patterns and best practices for Next.js/React applications. Use this skill when implementing animations, transitions, hover effects, page transitions, modals, or any. Next.js Animations Overview This skill provides comprehensive guidance for implementing smooth, performant, and accessible animations in Next.js and React applications. It covers CSS animations, Framer Motion, easing principles, and accessibility considerations. Invoke when the user asks about animate or related SKILL.md workflows.
- references/easing-and-timing.md - Easing functions, timing guidelines, spring configuration.
- references/css-animations.md - Transforms, transitions, keyframes, clip-path.
- references/framer-motion.md - Motion components, AnimatePresence, variants, layout animations, hooks.
- references/performance-accessibility.md - 60fps optimization, prefers-reduced-motion, accessibility.
Animate by the numbers
- 2,282 all-time installs (skills.sh)
- +172 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #209 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
animate capabilities & compatibility
- Capabilities
- references/easing and timing.md easing functio · references/css animations.md transforms, trans · references/framer motion.md motion components, · references/performance accessibility.md 60fps
What animate says it does
Animation patterns and best practices for Next.js/React applications. Use this skill when implementing animations, transitions, hover effects, page transitions, modals, or any moti
Animation patterns and best practices for Next.js/React applications. Use this skill when implementing animations, transitions, hover effects, page transitions,
npx skills add https://github.com/delphi-ai/animate-skill --skill animateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 75 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 28, 2026 |
| Repository | delphi-ai/animate-skill ↗ |
How do I animation patterns and best practices for next.js/react applications. use this skill when implementing animations, transitions, hover effects, page transitions, modals, or any?
Animation patterns and best practices for Next.js/React applications. Use this skill when implementing animations, transitions, hover effects, page transitions, modals, or any.
Who is it for?
Developers using animate workflows documented in SKILL.md.
Skip if: Skip when the task falls outside animate scope or needs a different stack.
When should I use this skill?
User asks about animate or related SKILL.md workflows.
What you get
Completed animate workflow with documented commands, files, and expected deliverables.
- Framer Motion component patterns
- easing reference tables
- accessible animation implementations
Files
Next.js Animations
Overview
This skill provides comprehensive guidance for implementing smooth, performant, and accessible animations in Next.js and React applications. It covers CSS animations, Framer Motion, easing principles, and accessibility considerations.
Quick Reference
Easing Cheat Sheet
| Animation Type | Easing | Duration |
|---|---|---|
| Element entering | ease-out | 200-300ms |
| Element moving on screen | ease-in-out | 200-300ms |
| Element exiting | ease-in | 150-200ms |
| Hover effects | ease | 150ms |
| Opacity only | linear | varies |
CSS Custom Properties (Recommended)
:root {
--ease-out-quint: cubic-bezier(.23, 1, .32, 1);
--ease-in-out-cubic: cubic-bezier(.645, .045, .355, 1);
--ease-out-cubic: cubic-bezier(.33, 1, .68, 1);
}Common Animation Patterns
1. Hover Lift Effect
.card {
transition: transform 200ms var(--ease-out-quint),
box-shadow 200ms var(--ease-out-quint);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}2. Button Press
.button {
transition: transform 100ms ease-out;
}
.button:active {
transform: scale(0.97);
}3. Fade In on Mount (Framer Motion)
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: [.23, 1, .32, 1] }}
>
Content
</motion.div>4. Modal with Exit Animation
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{children}
</motion.div>
)}
</AnimatePresence>5. Tab Indicator (Shared Layout)
{tabs.map(tab => (
<button key={tab} onClick={() => setActive(tab)} className="relative px-4 py-2">
{tab}
{active === tab && (
<motion.div
layoutId="tab-indicator"
className="absolute inset-0 bg-blue-500 rounded -z-10"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
</button>
))}6. Staggered List Animation
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
}
const item = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map(i => <motion.li key={i} variants={item}>{i}</motion.li>)}
</motion.ul>Golden Rules
1. Exits faster than enters: Exit animations should be ~75% of enter duration 2. Only animate transform and opacity: These are GPU-accelerated 3. 200-300ms is the sweet spot: Most animations should be in this range 4. Always respect prefers-reduced-motion: See accessibility section in references 5. Use springs for interruptible animations: Better UX when users interrupt
Examples
Complete working examples from the course are in the examples/ directory:
| Example | Description | Key Techniques |
|---|---|---|
card-hover.tsx | Slide-up description on hover | CSS transitions, transform, opacity |
toast-stacking.tsx | Animated toast notifications | CSS custom properties, data-* triggers |
text-reveal.tsx | Staggered letter animation | @keyframes, animation-delay, calc() |
shared-layout.tsx | Element position/size morph | Framer Motion layoutId |
animate-height.tsx | Smooth height changes | useMeasure, animate height |
multi-step-flow.tsx | Directional step wizard | AnimatePresence, custom variants |
feedback-popover.tsx | Button-to-popover expansion | Nested layoutId, form states |
app-store-card.tsx | iOS-style card expansion | Multiple layoutId elements |
To use an example, read it with: Read examples/<name>.tsx
References
For detailed documentation, read the reference files:
references/easing-and-timing.md- Easing functions, timing guidelines, spring configurationreferences/css-animations.md- Transforms, transitions, keyframes, clip-pathreferences/framer-motion.md- Motion components, AnimatePresence, variants, layout animations, hooksreferences/performance-accessibility.md- 60fps optimization, prefers-reduced-motion, accessibility
When to Use What
| Scenario | Recommended Approach |
|---|---|
| Simple hover effects | CSS transitions |
| Enter/exit animations | Framer Motion + AnimatePresence |
| Layout changes | Framer Motion layout prop |
| Shared element transitions | Framer Motion layoutId |
| Scroll-linked animations | Framer Motion useScroll |
| Complex orchestrated animations | Framer Motion variants |
| Drag interactions | Framer Motion drag gestures |
| Performance-critical | CSS-only with transforms |
Dependencies
For Framer Motion examples, install:
pnpm add framer-motion react-use-measure usehooks-ts/**
* Animate Height
*
* Smoothly animate height changes when content expands/collapses.
* Uses react-use-measure to get the actual content height.
*
* Key techniques:
* - useMeasure hook to get dynamic content dimensions
* - Animate height property (normally discouraged, but acceptable here)
* - Inner wrapper ref for measuring, outer wrapper for animating
* - Works for any dynamic content changes
*
* Note: Install react-use-measure: pnpm add react-use-measure
*/
"use client";
import { motion } from "framer-motion";
import { useState } from "react";
import useMeasure from "react-use-measure";
// styles.css
const styles = `
.wrapper {
display: flex;
flex-direction: column;
gap: 24px;
align-items: center;
}
.element {
background: #f5f5f5;
border-radius: 12px;
overflow: hidden;
width: 300px;
}
.inner {
padding: 16px;
}
`;
export default function AnimateHeight() {
const [showExtraContent, setShowExtraContent] = useState(false);
const [ref, bounds] = useMeasure();
return (
<div className="wrapper">
<button onClick={() => setShowExtraContent((b) => !b)}>
Toggle height
</button>
{/* Outer div animates to measured height */}
<motion.div
className="element"
animate={{ height: bounds.height }}
transition={{ duration: 0.3 }}
>
{/* Inner div is measured */}
<div className="inner" ref={ref}>
<h1>Expandable Drawer</h1>
<p>
This content can grow and shrink. The animation smoothly
transitions between heights.
</p>
{showExtraContent && (
<p>
This extra content will change the height of the drawer.
The animation handles any content changes automatically.
</p>
)}
</div>
</motion.div>
</div>
);
}
/**
* App Store Card Expansion
*
* iOS App Store-style card that expands to full screen with shared
* layout animation. Multiple elements animate together.
*
* Key techniques:
* - Multiple layoutId elements that animate together
* - whileTap for press feedback
* - Overlay with separate AnimatePresence
* - useOnClickOutside and Escape key for dismiss
* - borderRadius in style prop to prevent distortion
*/
"use client";
import { useState, useEffect, useRef } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { useOnClickOutside } from "usehooks-ts";
type Card = {
title: string;
description: string;
longDescription: string;
image: string;
};
// Card in grid view
function Card({
card,
setActiveCard,
}: {
card: Card;
setActiveCard: (card: Card | null) => void;
}) {
return (
<motion.div
layoutId={`card-${card.title}`}
className="card"
whileTap={{ scale: 0.98 }}
onClick={() => setActiveCard(card)}
style={{ borderRadius: 20 }}
>
<motion.img
layoutId={`image-${card.title}`}
src={card.image}
alt=""
style={{ borderRadius: 20 }}
draggable={false}
/>
<motion.div layoutId={`card-content-${card.title}`} className="card-content">
<motion.h2 layoutId={`card-heading-${card.title}`}>
{card.title}
</motion.h2>
<motion.p layoutId={`card-description-${card.title}`}>
{card.description}
</motion.p>
</motion.div>
{/* Hidden long description - will animate in when expanded */}
<motion.div
layoutId={`card-long-description-${card.title}`}
style={{ position: "absolute", top: "100%", opacity: 0 }}
>
{card.longDescription}
</motion.div>
</motion.div>
);
}
// Expanded card view
function ActiveCard({
activeCard,
setActiveCard,
}: {
activeCard: Card;
setActiveCard: (card: Card | null) => void;
}) {
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setActiveCard(null));
return (
<motion.div
ref={ref}
layoutId={`card-${activeCard.title}`}
className="card card-active"
style={{ borderRadius: 0 }}
>
<motion.img
layoutId={`image-${activeCard.title}`}
src={activeCard.image}
alt=""
style={{ borderRadius: 0 }}
/>
<motion.button
className="close-button"
onClick={() => setActiveCard(null)}
>
✕
</motion.button>
<motion.div layoutId={`card-content-${activeCard.title}`} className="card-content">
<motion.h2 layoutId={`card-heading-${activeCard.title}`}>
{activeCard.title}
</motion.h2>
<motion.p layoutId={`card-description-${activeCard.title}`}>
{activeCard.description}
</motion.p>
</motion.div>
{/* Long description now visible */}
<motion.div layoutId={`card-long-description-${activeCard.title}`}>
{activeCard.longDescription}
</motion.div>
</motion.div>
);
}
export default function AppStoreCards() {
const [activeCard, setActiveCard] = useState<Card | null>(null);
// Dismiss on Escape
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") setActiveCard(null);
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
return (
<div className="cards-wrapper">
{CARDS.map((card) => (
<Card key={card.title} card={card} setActiveCard={setActiveCard} />
))}
{/* Overlay */}
<AnimatePresence>
{activeCard && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="overlay"
/>
)}
</AnimatePresence>
{/* Expanded card */}
<AnimatePresence>
{activeCard && (
<ActiveCard activeCard={activeCard} setActiveCard={setActiveCard} />
)}
</AnimatePresence>
</div>
);
}
const CARDS: Card[] = [
{
title: "Game Title",
description: "A brief description",
longDescription: "Full description with more details about the game...",
image: "/game-image.webp",
},
];
/**
* Card Hover Animation
*
* A card component where the description slides up from the bottom on hover.
* Uses CSS transitions with transform and opacity for smooth animation.
*
* Key techniques:
* - Transform translateY to slide content in/out
* - Combined transition on transform and opacity
* - :hover and :focus-visible for accessibility
* - overflow: hidden on parent to clip content
*/
// card-hover.css
const styles = `
.card-hover {
width: 340px;
height: 340px;
border-radius: 16px;
background: #fff;
display: flex;
align-items: flex-end;
text-decoration: none;
box-shadow: 0px 0px 0px 1px rgba(9, 9, 11, 0.08),
0px 1px 2px -1px rgba(9, 9, 11, 0.08),
0px 2px 4px 0px rgba(9, 9, 11, 0.04);
overflow: hidden;
}
.card-hover-description {
border-radius: 12px;
border: 1px solid #fff;
position: relative;
background: #fafafa;
margin: 6px;
width: 100%;
padding: 10px 14px 13px;
font-size: 13px;
box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08),
0px 1px 2px -1px rgba(0, 0, 0, 0.08),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
/* Animation - hidden by default */
transform: translateY(calc(100% + 8px));
transition-duration: 350ms;
transition-timing-function: ease;
transition-property: transform, opacity;
opacity: 0;
}
/* Show on hover or keyboard focus */
.card-hover:hover .card-hover-description,
.card-hover:focus-visible .card-hover-description {
transform: translateY(0%);
opacity: 1;
}
.card-hover-title {
color: #1b1b1d;
font-weight: 500;
}
.card-hover-subtitle {
color: #717175;
line-height: 1;
margin-top: 4px;
}
`;
export default function CardHover() {
return (
<a href="#" className="card-hover">
<div className="card-hover-description">
<h3 className="card-hover-title">Project name</h3>
<p className="card-hover-subtitle">Project description</p>
</div>
</a>
);
}
/**
* Feedback Popover Animation
*
* A button that expands into a feedback form using shared layout animation.
* Shows loading and success states with animated transitions.
*
* Key techniques:
* - layoutId on button and popover for seamless expansion
* - Nested layoutId elements (text placeholder)
* - AnimatePresence for popover enter/exit
* - Form state transitions (idle -> loading -> success)
* - useOnClickOutside for dismiss behavior
*
* Note: Install usehooks-ts: pnpm add usehooks-ts
*/
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState, useRef } from "react";
import { useOnClickOutside } from "usehooks-ts";
export default function FeedbackPopover() {
const [open, setOpen] = useState(false);
const [formState, setFormState] = useState<"idle" | "loading" | "success">("idle");
const [feedback, setFeedback] = useState("");
const ref = useRef<HTMLDivElement>(null);
// Close on click outside
useOnClickOutside(ref, () => setOpen(false));
function submit() {
setFormState("loading");
setTimeout(() => setFormState("success"), 1500);
setTimeout(() => setOpen(false), 3300);
}
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
if ((event.ctrlKey || event.metaKey) && event.key === "Enter" && open && formState === "idle") {
submit();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, formState]);
return (
<div className="feedback-wrapper">
{/* Button with layoutId - will animate into popover */}
<motion.button
onClick={() => {
setOpen(true);
setFormState("idle");
setFeedback("");
}}
className="feedback-button"
style={{ borderRadius: 8 }}
layoutId="background"
>
<motion.span layoutId="placeholder">
Feedback
</motion.span>
</motion.button>
<AnimatePresence>
{open && (
<motion.div
ref={ref}
className="feedback-popover"
style={{ borderRadius: 12 }}
layoutId="background"
>
{/* Placeholder text animates with the layout */}
<motion.span
aria-hidden
className="placeholder"
layoutId="placeholder"
style={{ opacity: feedback ? 0 : 0.5 }}
>
Feedback
</motion.span>
{formState === "success" ? (
<div className="success-wrapper">
<h3>Feedback received!</h3>
<p>Thanks for your feedback.</p>
</div>
) : (
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
<textarea
autoFocus
placeholder="Feedback"
onChange={(e) => setFeedback(e.target.value)}
value={feedback}
required
/>
<button type="submit">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={formState}
initial={{ opacity: 0, y: -25 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 25 }}
transition={{ type: "spring", duration: 0.3, bounce: 0 }}
>
{formState === "loading" ? "Sending..." : "Send feedback"}
</motion.span>
</AnimatePresence>
</button>
</form>
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
/**
* Multi-Step Flow Animation
*
* Animated wizard/stepper with directional slide transitions.
* Steps slide in from the direction of navigation.
*
* Key techniques:
* - AnimatePresence mode="popLayout" for exit animations
* - custom prop for directional variants (slide left or right)
* - useMeasure for animating container height
* - useReducedMotion for accessibility
* - MotionConfig for shared transition settings
*/
"use client";
import { useMemo, useState } from "react";
import {
AnimatePresence,
MotionConfig,
motion,
useReducedMotion,
} from "framer-motion";
import useMeasure from "react-use-measure";
// Directional slide variants
const variants = {
initial: (direction: number) => ({
x: `${110 * direction}%`,
opacity: 0,
}),
active: {
x: "0%",
opacity: 1,
},
exit: (direction: number) => ({
x: `${-110 * direction}%`,
opacity: 0,
}),
};
// Reduced motion alternative - just fade
const reducedMotionVariants = {
initial: { opacity: 0 },
active: { opacity: 1 },
exit: { opacity: 0 },
};
export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);
const [direction, setDirection] = useState<number>(0);
const [ref, bounds] = useMeasure();
const reducedMotion = useReducedMotion();
const content = useMemo(() => {
switch (currentStep) {
case 0:
return (
<>
<h2>Step One</h2>
<p>First step content goes here.</p>
</>
);
case 1:
return (
<>
<h2>Step Two</h2>
<p>Second step with different content length.</p>
<p>Extra paragraph to show height animation.</p>
</>
);
case 2:
return (
<>
<h2>Step Three</h2>
<p>Final step - you made it!</p>
</>
);
}
}, [currentStep]);
return (
<MotionConfig transition={{ duration: 0.5, type: "spring", bounce: 0 }}>
{/* Container animates height based on content */}
<motion.div
className="multi-step-wrapper"
animate={reducedMotion ? {} : { height: bounds.height }}
>
<div className="multi-step-inner" ref={ref}>
{/* AnimatePresence handles exit animations */}
<AnimatePresence mode="popLayout" initial={false} custom={direction}>
<motion.div
key={currentStep}
variants={reducedMotion ? reducedMotionVariants : variants}
initial="initial"
animate="active"
exit="exit"
custom={direction}
>
{content}
</motion.div>
</AnimatePresence>
{/* Buttons use layout animation to stay in place */}
<motion.div className="actions" layout={!reducedMotion}>
<button
disabled={currentStep === 0}
onClick={() => {
setCurrentStep((prev) => prev - 1);
setDirection(-1); // Slide from left
}}
>
Back
</button>
<button
disabled={currentStep === 2}
onClick={() => {
setCurrentStep((prev) => prev + 1);
setDirection(1); // Slide from right
}}
>
Continue
</button>
</motion.div>
</div>
</motion.div>
</MotionConfig>
);
}
/**
* Shared Layout Animation
*
* Demonstrates Framer Motion's layoutId for seamless transitions
* between two different elements/positions.
*
* Key techniques:
* - layoutId creates a shared identity between elements
* - Motion automatically animates position and size changes
* - style={{ borderRadius }} prevents border radius distortion during animation
*/
"use client";
import { motion } from "framer-motion";
import { useState } from "react";
// styles.css
const styles = `
.wrapper {
display: flex;
flex-direction: column;
gap: 24px;
align-items: center;
}
.element {
width: 100px;
height: 100px;
background: #3b82f6;
}
.second-element {
width: 200px;
height: 200px;
background: #3b82f6;
}
`;
export default function SharedLayoutExample() {
const [showSecond, setShowSecond] = useState(false);
return (
<div className="wrapper">
<button onClick={() => setShowSecond((s) => !s)}>
Animate
</button>
{/* Same layoutId = smooth transition between the two */}
{showSecond ? (
<motion.div
layoutId="rectangle"
className="second-element"
style={{ borderRadius: 12 }}
/>
) : (
<motion.div
layoutId="rectangle"
className="element"
style={{ borderRadius: 12 }}
/>
)}
</div>
);
}
/**
* Text Reveal Animation
*
* Staggered letter-by-letter text reveal animation using CSS keyframes.
* Each letter animates in sequence with a delay based on its index.
*
* Key techniques:
* - Split text into individual spans
* - CSS custom property (--index) for stagger delay
* - @keyframes for the reveal animation
* - animation-fill-mode: backwards to hide before animation
* - cubic-bezier for smooth easing
*/
"use client";
import { useState } from "react";
// text-reveal.css
const styles = `
.h1 {
font-size: 4rem;
font-weight: bold;
overflow: hidden;
}
.h1 span {
display: inline-block;
animation: reveal 1.3s cubic-bezier(0.19, 1, 0.22, 1);
animation-fill-mode: backwards;
animation-delay: calc(var(--index) * 0.03s);
}
@keyframes reveal {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
`;
const WORD = "Animations";
export default function TextReveal() {
const [reset, setReset] = useState(0);
return (
<div className="flex flex-col gap-4">
{/* Key change forces re-render and replays animation */}
<div key={reset}>
<h1 className="h1">
{WORD.split("").map((char, index) => (
<span
key={index}
style={{ "--index": index } as React.CSSProperties}
>
{char}
</span>
))}
</h1>
</div>
<button onClick={() => setReset(reset + 1)}>
Replay animation
</button>
</div>
);
}
/**
* Toast Stacking Animation
*
* Animated toast notifications that stack vertically with smooth transitions.
* Uses CSS custom properties (--index) for dynamic positioning.
*
* Key techniques:
* - CSS custom properties for dynamic values
* - data-* attributes to trigger CSS transitions
* - useEffect to trigger mount animation
* - calc() for computing stack positions
*/
"use client";
import { useEffect, useState } from "react";
// toast-animation.css
const styles = `
.toast {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 14px 13px;
width: 100%;
font-size: 13px;
border-radius: 8px;
background: white;
box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08),
0px 1px 2px -1px rgba(0, 0, 0, 0.08),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
/* Animation - starts hidden below */
transition-property: transform, opacity;
transition-duration: 350ms;
transition-timing-function: ease;
transform: translateY(100%);
opacity: 0;
}
/* Animate to stacked position when mounted */
.toast[data-mounted="true"] {
transform: translateY(calc(var(--index) * (100% + var(--gap)) * -1));
opacity: 1;
}
.toaster {
position: absolute;
left: 50%;
bottom: 80px;
display: flex;
flex-direction: column;
gap: var(--gap);
width: 356px;
transform: translateX(-50%);
--gap: 16px;
}
`;
export default function Toaster() {
const [toasts, setToasts] = useState(0);
return (
<div className="toast-wrapper">
<div className="toaster">
{Array.from({ length: toasts }).map((_, i) => (
<Toast key={i} index={toasts - (i + 1)} />
))}
</div>
<button onClick={() => setToasts(toasts + 1)}>
Add toast
</button>
</div>
);
}
function Toast({ index }: { index: number }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div
className="toast"
style={{ "--index": index } as React.CSSProperties}
data-mounted={mounted}
>
<span className="title">Event Created</span>
<span className="description">Monday, January 3rd at 6:00pm</span>
</div>
);
}
Animate Skill for Claude Code
Animation patterns and best practices for Next.js/React applications. Based on Emil Kowalski's "Animations on the Web" course.
Installation
npx skills add https://github.com/delphi-ai/animate-skill --skill animateWhat's Included
- SKILL.md - Quick reference for easing, timing, and common patterns
- examples/ - 8 complete working examples (hover effects, toasts, modals, etc.)
- references/ - Detailed docs on CSS animations, Framer Motion, performance, accessibility
Usage
Once installed, the skill triggers automatically when you ask Claude Code to implement animations, transitions, hover effects, modals, or any motion in React components.
You can also invoke it directly with /animate.
Examples Included
| Example | Description |
|---|---|
card-hover.tsx | Slide-up description on hover |
toast-stacking.tsx | Animated toast notifications |
text-reveal.tsx | Staggered letter animation |
shared-layout.tsx | Framer Motion layoutId morph |
animate-height.tsx | Smooth height changes |
multi-step-flow.tsx | Directional step wizard |
feedback-popover.tsx | Button-to-popover expansion |
app-store-card.tsx | iOS-style card expansion |
Dependencies
For Framer Motion examples:
pnpm add framer-motion react-use-measure usehooks-tsCSS Animations Reference
Transforms
Transforms change an element's position, size, or rotation without affecting layout. They are GPU-accelerated and performant.
Transform Functions
/* Translation (movement) */
transform: translateX(10px);
transform: translateY(-20px);
transform: translate(10px, -20px); /* X, Y */
transform: translate3d(10px, 20px, 30px); /* X, Y, Z */
/* Scale */
transform: scale(1.1); /* Uniform scale */
transform: scaleX(0.5);
transform: scaleY(1.5);
transform: scale(0.5, 1.5); /* X, Y */
/* Rotation */
transform: rotate(45deg);
transform: rotateX(45deg); /* 3D - around X axis */
transform: rotateY(45deg); /* 3D - around Y axis */
transform: rotateZ(45deg); /* Same as rotate() */
/* Combining transforms */
transform: translateY(-10px) scale(1.05) rotate(5deg);Transform Origin
Controls the point around which transforms occur:
transform-origin: center; /* Default */
transform-origin: top left;
transform-origin: 50% 100%; /* Center bottom */
transform-origin: 0 0; /* Top left corner */3D Transforms
/* Enable 3D space on parent */
.parent {
perspective: 1000px; /* Distance from viewer */
perspective-origin: center;
}
/* 3D transforms on children */
.child {
transform: rotateY(45deg);
transform-style: preserve-3d; /* Maintain 3D for nested elements */
backface-visibility: hidden; /* Hide back of element */
}Transitions
Transitions animate property changes over time.
Syntax
/* Individual properties */
transition-property: transform, opacity;
transition-duration: 200ms;
transition-timing-function: ease-out;
transition-delay: 0ms;
/* Shorthand */
transition: transform 200ms ease-out, opacity 150ms ease-out 50ms;
/* property duration timing property duration timing delay */Common Transition Patterns
/* Hover lift effect */
.card {
transition: transform 200ms ease-out, box-shadow 200ms ease-out;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
/* Button press effect */
.button {
transition: transform 100ms ease-out;
}
.button:active {
transform: scale(0.97);
}
/* Fade in/out */
.element {
opacity: 0;
transition: opacity 200ms ease-out;
}
.element.visible {
opacity: 1;
}Performance-Safe Properties
These properties are GPU-accelerated and won't cause layout recalculation:
transformopacity
Avoid animating (cause layout/paint):
width,heighttop,left,right,bottommargin,paddingborder-width
Keyframe Animations
For complex, multi-step animations.
Basic Syntax
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.element {
animation: fadeIn 300ms ease-out forwards;
}Animation Properties
animation-name: fadeIn;
animation-duration: 300ms;
animation-timing-function: ease-out;
animation-delay: 0ms;
animation-iteration-count: 1; /* or 'infinite' */
animation-direction: normal; /* or 'reverse', 'alternate' */
animation-fill-mode: forwards; /* 'none', 'forwards', 'backwards', 'both' */
animation-play-state: running; /* or 'paused' */
/* Shorthand */
animation: fadeIn 300ms ease-out 0ms 1 normal forwards;Fill Modes Explained
none: No styles applied before/after animationforwards: Retains final keyframe styles after animationbackwards: Applies initial keyframe styles during delayboth: Applies both forwards and backwards
Clip-Path Animations
Clip-path creates masked areas that can be animated.
Common Shapes
/* Circle */
clip-path: circle(50% at center);
clip-path: circle(0% at center); /* Hidden */
/* Ellipse */
clip-path: ellipse(50% 30% at center);
/* Inset (rectangle) */
clip-path: inset(0); /* Full visibility */
clip-path: inset(50%); /* Hidden (collapsed to center) */
clip-path: inset(0 50% 0 0); /* Right half hidden */
/* inset(top right bottom left) */
/* Polygon */
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); /* Rectangle */
clip-path: polygon(50% 0, 100% 100%, 0 100%); /* Triangle */Clip-Path Animation Example (Image Comparison Slider)
.before-image {
clip-path: inset(0 50% 0 0); /* Show left half */
transition: clip-path 0ms; /* Instant update */
}
/* JavaScript updates the clip-path based on slider position */Tab Indicator with Clip-Path
// Active tab background slides using clip-path
<div className="relative">
{tabs.map((tab, index) => (
<button
key={tab}
onClick={() => setActive(index)}
className="relative z-10 px-4 py-2"
>
{tab}
</button>
))}
<div
className="absolute inset-0 bg-blue-500 rounded transition-all duration-200"
style={{
clipPath: `inset(0 ${100 - (activeIndex + 1) * (100 / tabs.length)}% 0 ${activeIndex * (100 / tabs.length)}% round 8px)`
}}
/>
</div>Easing and Timing Reference
Core Principles
Animations should feel natural and purposeful. The goal is to make interfaces feel responsive without drawing attention to the animation itself.
Easing Functions
When to Use Each Easing Type
| Easing | Use Case | Timing |
|---|---|---|
ease-out | Elements entering the screen | 200-300ms |
ease-in-out | Elements moving on screen | 200-300ms |
ease-in | Elements exiting the screen | 150-200ms |
ease | Hover effects | 150ms |
linear | Opacity changes, progress bars | varies |
Custom Easing Variables
Define these CSS custom properties for consistent animations:
:root {
/* Recommended easing curves */
--ease-out-quint: cubic-bezier(.23, 1, .32, 1);
--ease-in-out-cubic: cubic-bezier(.645, .045, .355, 1);
--ease-out-cubic: cubic-bezier(.33, 1, .68, 1);
--ease-in-cubic: cubic-bezier(.32, 0, .67, 0);
/* Spring-like feel */
--ease-out-back: cubic-bezier(.34, 1.56, .64, 1);
}Cubic Bezier Explained
cubic-bezier(x1, y1, x2, y2) defines a curve with two control points:
(x1, y1)- First control point (influences start of animation)(x2, y2)- Second control point (influences end of animation)
Values > 1 create overshoot effects (like ease-out-back).
Timing Guidelines
Duration Sweet Spots
- General animations: 200-300ms (the sweet spot)
- Hover effects: 150ms
- Modal enter: 200ms
- Modal exit: 150ms (exits should be faster than enters)
- Page transitions: 300-400ms
- Micro-interactions: 100-150ms
Rules of Thumb
1. Exits faster than enters: Exit animations should be ~75% of enter duration 2. Smaller elements = faster animations: Scale duration with element size 3. User-initiated = faster response: Direct actions should feel immediate 4. System-initiated = can be slower: Background transitions can take longer
Spring Animations
Springs are excellent for interruptible animations and natural-feeling motion.
Spring Parameters
| Parameter | Effect |
|---|---|
mass | Weight of the object (higher = slower, more momentum) |
tension (stiffness) | Spring tightness (higher = faster, snappier) |
friction (damping) | Resistance (higher = less oscillation) |
velocity | Initial speed |
Framer Motion Spring Config
// Snappy spring (UI elements)
const snappy = { type: "spring", stiffness: 400, damping: 30 }
// Gentle spring (larger elements)
const gentle = { type: "spring", stiffness: 200, damping: 20 }
// Bouncy spring (playful interactions)
const bouncy = { type: "spring", stiffness: 300, damping: 10 }Duration-Based Springs (Framer Motion)
// Using duration instead of physics
const durationSpring = {
type: "spring",
duration: 0.3,
bounce: 0.2 // 0 = no bounce, 1 = max bounce
}When NOT to Animate
- Loading states that block interaction
- Error messages that need immediate attention
- Accessibility: respect
prefers-reduced-motion - When animation adds no value to the experience
- Repeated actions the user performs frequently
Framer Motion Reference
Framer Motion is the recommended animation library for React/Next.js applications.
Installation
npm install framer-motion
# or
pnpm add framer-motionBasic Usage
Motion Components
import { motion } from "framer-motion"
// Any HTML element can become animated
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
Content
</motion.div>Common Props
| Prop | Description |
|---|---|
initial | Starting state (or false to disable) |
animate | Target state |
exit | Exit state (requires AnimatePresence) |
transition | Animation configuration |
whileHover | State while hovered |
whileTap | State while pressed |
whileFocus | State while focused |
whileInView | State while in viewport |
AnimatePresence
Required for exit animations. Wraps elements that may be removed from DOM.
import { motion, AnimatePresence } from "framer-motion"
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
)
}AnimatePresence Props
<AnimatePresence
mode="wait" // 'sync' | 'wait' | 'popLayout'
initial={false} // Disable initial animation on mount
onExitComplete={() => {}} // Callback when all exits complete
>mode="sync"(default): Enter and exit happen simultaneouslymode="wait": Wait for exit to complete before entermode="popLayout": Remove exiting elements from layout flow
Variants
Variants define animation states that can be orchestrated across parent/children.
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Delay between each child
delayChildren: 0.2, // Initial delay before children start
}
}
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
function List({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
)
}Layout Animations
Animate layout changes automatically with the layout prop.
// Element animates when its position/size changes
<motion.div layout>
{isExpanded ? "Expanded content here" : "Collapsed"}
</motion.div>
// Layout types
<motion.div layout> // Animate position and size
<motion.div layout="position"> // Only animate position
<motion.div layout="size"> // Only animate sizeShared Layout Animations (layoutId)
Elements with the same layoutId animate between each other.
function Tabs({ tabs, activeTab, setActiveTab }) {
return (
<div className="flex gap-2">
{tabs.map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className="relative px-4 py-2"
>
{tab}
{activeTab === tab && (
<motion.div
layoutId="tab-indicator"
className="absolute inset-0 bg-blue-500 rounded -z-10"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
</button>
))}
</div>
)
}LayoutGroup
Group layout animations to prevent conflicts between independent animations.
import { LayoutGroup } from "framer-motion"
<LayoutGroup>
<Tabs /> {/* These layout animations */}
</LayoutGroup>
<LayoutGroup>
<OtherTabs /> {/* Won't conflict with these */}
</LayoutGroup>Gestures
Drag
<motion.div
drag // Enable both axes
drag="x" // Horizontal only
drag="y" // Vertical only
dragConstraints={{ left: 0, right: 300, top: 0, bottom: 200 }}
dragElastic={0.2} // Elasticity outside constraints (0-1)
dragMomentum={true} // Continue after release
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }}
onDragStart={(event, info) => {}}
onDrag={(event, info) => {}}
onDragEnd={(event, info) => {}}
>
Drag me
</motion.div>Drag Info Object
onDrag={(event, info) => {
info.point // { x, y } current position
info.delta // { x, y } change since last frame
info.offset // { x, y } offset from drag start
info.velocity // { x, y } current velocity
}}Motion Values & Hooks
useMotionValue
Create a value that updates without re-rendering.
import { motion, useMotionValue, useTransform } from "framer-motion"
function Component() {
const x = useMotionValue(0)
// Transform x position to opacity
const opacity = useTransform(x, [-100, 0, 100], [0, 1, 0])
// Transform x to rotation
const rotate = useTransform(x, [-100, 100], [-10, 10])
return (
<motion.div
drag="x"
style={{ x, opacity, rotate }}
/>
)
}useSpring
Create a spring-animated motion value.
import { useSpring, useMotionValue } from "framer-motion"
function Component() {
const x = useMotionValue(0)
const springX = useSpring(x, { stiffness: 300, damping: 30 })
return (
<motion.div
onMouseMove={(e) => x.set(e.clientX)}
style={{ x: springX }}
/>
)
}useScroll
Track scroll progress.
import { useScroll, useTransform, motion } from "framer-motion"
function ProgressBar() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
)
}
// Scroll within a container
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end start"] // When to start/end tracking
})useInView
Detect when element enters viewport.
import { useInView } from "framer-motion"
function Component() {
const ref = useRef(null)
const isInView = useInView(ref, {
once: true, // Only trigger once
margin: "-100px" // Trigger 100px before entering
})
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
/>
)
}Transition Options
const transition = {
// Timing
duration: 0.3,
delay: 0.1,
// Easing
ease: "easeOut", // or array: [0.23, 1, 0.32, 1]
// Spring physics
type: "spring",
stiffness: 400,
damping: 30,
mass: 1,
// Or duration-based spring
type: "spring",
duration: 0.3,
bounce: 0.2,
// Per-property transitions
opacity: { duration: 0.2 },
x: { type: "spring", stiffness: 300 },
// Repeat
repeat: Infinity,
repeatType: "reverse", // "loop" | "reverse" | "mirror"
repeatDelay: 0.5,
}Multi-Step Animations (Keyframes)
<motion.div
animate={{
x: [0, 100, 0], // Move right then back
opacity: [0, 1, 1, 0], // Fade in, stay, fade out
scale: [1, 1.2, 1], // Grow then shrink
}}
transition={{
duration: 2,
times: [0, 0.5, 1], // When each keyframe occurs (0-1)
ease: "easeInOut",
}}
/>Performance & Accessibility Reference
Performance
Target: 60 FPS
Animations should run at 60 frames per second (16.67ms per frame) for smooth motion.
GPU-Accelerated Properties
Only these properties are hardware-accelerated and won't cause layout recalculation:
| Property | Notes |
|---|---|
transform | translate, scale, rotate |
opacity | Fade effects |
Properties to Avoid Animating
These trigger layout recalculation and are expensive:
width,heighttop,left,right,bottommargin,paddingborder-widthfont-size
Instead: Use transform: scale() instead of width/height, transform: translate() instead of top/left.
will-change
Hints to the browser that an element will animate:
.will-animate {
will-change: transform, opacity;
}Caution: Use sparingly. Apply only to elements that actually animate, and remove after animation completes if possible. Overuse can hurt performance.
Hardware Acceleration Hack
Force GPU layer creation (use sparingly):
.force-gpu {
transform: translateZ(0);
/* or */
backface-visibility: hidden;
}Monitoring Performance
1. Chrome DevTools: Performance tab → Record during animation 2. Rendering tab: Enable "FPS meter" and "Paint flashing" 3. Layers panel: Check layer composition
Common Performance Issues
| Issue | Solution |
|---|---|
| Janky scrolling | Use transform instead of top/left |
| Slow animations | Reduce number of animated elements |
| Memory spikes | Avoid creating new objects during animation |
| Layout thrashing | Batch DOM reads/writes |
Accessibility
prefers-reduced-motion
Users can indicate they prefer reduced motion in their system settings. Always respect this preference.
CSS Implementation
/* Full animation by default */
.element {
transition: transform 300ms ease-out;
}
/* Reduced or no motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
.element {
transition: none;
/* or minimal transition */
transition: opacity 200ms ease-out;
}
}React/Framer Motion Implementation
import { useReducedMotion } from "framer-motion"
function AnimatedComponent() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
/>
)
}Global Configuration with MotionConfig
import { MotionConfig, useReducedMotion } from "framer-motion"
function App() {
const shouldReduceMotion = useReducedMotion()
return (
<MotionConfig reducedMotion={shouldReduceMotion ? "always" : "never"}>
{/* All motion components inside respect this setting */}
<YourApp />
</MotionConfig>
)
}MotionConfig Options
| Value | Effect |
|---|---|
"never" | Normal animations (default) |
"always" | Skip all animations |
"user" | Respect system prefers-reduced-motion setting |
Accessible Animation Guidelines
1. Provide alternatives: Ensure content is accessible without animation 2. Avoid flashing: No content flashing more than 3 times per second 3. Don't block interaction: Animations shouldn't prevent users from interacting 4. Keep it brief: Long animations can be frustrating 5. Purposeful motion: Only animate when it adds value 6. Pause controls: For continuous animations, provide pause/stop
Focus Management with Animations
// Ensure focus moves appropriately with animated elements
function Modal({ isOpen }) {
const modalRef = useRef(null)
useEffect(() => {
if (isOpen) {
// Wait for enter animation before focusing
const timer = setTimeout(() => {
modalRef.current?.focus()
}, 300)
return () => clearTimeout(timer)
}
}, [isOpen])
return (
<AnimatePresence>
{isOpen && (
<motion.div
ref={modalRef}
tabIndex={-1}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>
)
}Testing Reduced Motion
1. macOS: System Preferences → Accessibility → Display → Reduce motion 2. Windows: Settings → Ease of Access → Display → Show animations 3. iOS: Settings → Accessibility → Motion → Reduce Motion 4. Chrome DevTools: Rendering tab → Emulate CSS media feature prefers-reduced-motion
Related skills
How it compares
Pick animate for React and Next.js UI motion patterns; use Remotion or game-animation skills for video or canvas-based motion.
FAQ
What does animate do?
Animation patterns and best practices for Next.js/React applications. Use this skill when implementing animations, transitions, hover effects, page transitions, modals, or any.
When should I use animate?
User asks about animate or related SKILL.md workflows.
Is animate safe to install?
Review the Security Audits panel on this page before installing in production.