react-spring is a physics-based animation library for React — useSpring({ opacity: 1, transform: "translateY(0px)" }) returns animated values. animated.div renders the animated styles. config.wobbly gives a bouncy spring feel; config.gentle is smooth. useSprings(count, items.map(...)) animates multiple independent springs. useTrail(count, config) staggers animations across a list with the last spring driving the first. useTransition(items, { from, enter, leave }) handles mount and unmount animations. useChain([springRef, trailRef], [0, 0.3]) sequences animations. interpolate maps spring values to complex CSS. @use-gesture/react binds drag, hover, and scroll gestures to spring targets with useDrag. useSpringRef() creates an imperative handle for sequenced chains. Claude Code generates react-spring page transitions, list animations, drag-to-dismiss cards, parallax hero sections, and staggered grid entrances.
CLAUDE.md for react-spring
## react-spring Stack
- Version: @react-spring/web >= 9.7, @use-gesture/react >= 10.3
- Basic: const [styles, api] = useSpring(() => ({ opacity: 0, y: 20, config: config.gentle }))
- Trigger: api.start({ opacity: 1, y: 0 }) — imperative API
- Render: <animated.div style={styles}>
- List: const springs = useSprings(items.length, items.map(item => ({ ... })))
- Trail: const trail = useTrail(items.length, { opacity: open ? 1 : 0, y: open ? 0 : 20 })
- Transition: useTransition(show, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 } })
- Gesture: const bind = useDrag(({ active, movement: [mx], velocity: [vx] }) => api.start(...))
- Config: config.gentle | config.wobbly | config.stiff | config.slow | { tension: 200, friction: 20 }
Animated Components
// components/animation/SpringCard.tsx — useSpring with hover and click
"use client"
import { useSpring, animated, config } from "@react-spring/web"
import { useState } from "react"
interface SpringCardProps {
title: string
description: string
onClick?: () => void
}
export function SpringCard({ title, description, onClick }: SpringCardProps) {
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
const [styles, api] = useSpring(() => ({
scale: 1,
y: 0,
shadow: 4,
config: config.gentle,
}))
const handleMouseEnter = () => {
setHovered(true)
api.start({ scale: 1.03, y: -4, shadow: 16 })
}
const handleMouseLeave = () => {
setHovered(false)
api.start({ scale: 1, y: 0, shadow: 4 })
}
const handleClick = () => {
setClicked(true)
api.start({
scale: 0.96,
config: { tension: 400, friction: 10 },
onRest: () => {
api.start({ scale: 1.03, config: config.wobbly })
setClicked(false)
onClick?.()
},
})
}
return (
<animated.div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{
transform: styles.scale.to(s => `scale(${s})`).to(s => `${s} translateY(${styles.y.get()}px)`),
boxShadow: styles.shadow.to(s => `0 ${s}px ${s * 3}px rgba(0,0,0,0.1)`),
cursor: "pointer",
}}
className="rounded-2xl border bg-card p-6 select-none"
>
<h3 className="font-semibold text-lg">{title}</h3>
<p className="text-muted-foreground text-sm mt-2">{description}</p>
</animated.div>
)
}
Trail List Animation
// components/animation/TrailList.tsx — staggered list entrance
"use client"
import { useTrail, animated, config } from "@react-spring/web"
import { useEffect, useState } from "react"
interface TrailListProps {
items: { id: string; label: string; icon?: string }[]
}
export function TrailList({ items }: TrailListProps) {
const [open, setOpen] = useState(false)
useEffect(() => {
// Trigger entrance after mount
const t = setTimeout(() => setOpen(true), 100)
return () => clearTimeout(t)
}, [])
const trail = useTrail(items.length, {
opacity: open ? 1 : 0,
x: open ? 0 : 30,
scale: open ? 1 : 0.9,
config: { tension: 280, friction: 22 },
delay: 50,
})
return (
<ul className="space-y-2">
{trail.map((styles, index) => (
<animated.li
key={items[index].id}
style={{
opacity: styles.opacity,
transform: styles.x.to(x => `translateX(${x}px) scale(${styles.scale.get()})`),
}}
className="flex items-center gap-3 p-3 rounded-xl bg-muted/50 border"
>
{items[index].icon && (
<span className="text-xl">{items[index].icon}</span>
)}
<span className="font-medium text-sm">{items[index].label}</span>
</animated.li>
))}
</ul>
)
}
Mount/Unmount Transitions
// components/animation/ToastNotification.tsx — enter/leave transitions
"use client"
import { useTransition, animated, config } from "@react-spring/web"
import { useState, useCallback, useEffect } from "react"
type ToastType = "success" | "error" | "info"
interface Toast {
id: string
message: string
type: ToastType
}
const COLORS: Record<ToastType, string> = {
success: "bg-green-500",
error: "bg-red-500",
info: "bg-blue-500",
}
export function useToasts() {
const [toasts, setToasts] = useState<Toast[]>([])
const addToast = useCallback((message: string, type: ToastType = "info") => {
const id = crypto.randomUUID()
setToasts(prev => [...prev, { id, message, type }])
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id))
}, 3500)
}, [])
return { toasts, addToast }
}
export function ToastContainer({ toasts }: { toasts: Toast[] }) {
const transitions = useTransition(toasts, {
keys: toast => toast.id,
from: { opacity: 0, x: 60, scale: 0.85 },
enter: { opacity: 1, x: 0, scale: 1 },
leave: { opacity: 0, x: 60, scale: 0.85 },
config: config.stiff,
})
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 items-end">
{transitions((styles, toast) => (
<animated.div
style={{
opacity: styles.opacity,
transform: styles.x.to(x => `translateX(${x}px) scale(${styles.scale.get()})`),
}}
className={`${COLORS[toast.type]} text-white px-4 py-2.5 rounded-xl shadow-xl text-sm font-medium max-w-xs`}
>
{toast.message}
</animated.div>
))}
</div>
)
}
Drag Gesture with @use-gesture
// components/animation/DraggableCard.tsx — drag with velocity-based dismiss
"use client"
import { useSpring, animated } from "@react-spring/web"
import { useDrag } from "@use-gesture/react"
import { useState } from "react"
interface DraggableCardProps {
children: React.ReactNode
onDismiss?: () => void
}
export function DraggableCard({ children, onDismiss }: DraggableCardProps) {
const [dismissed, setDismissed] = useState(false)
const [{ x, rotate, opacity }, api] = useSpring(() => ({
x: 0,
rotate: 0,
opacity: 1,
config: { tension: 300, friction: 30 },
}))
const bind = useDrag(
({ active, movement: [mx], velocity: [vx], direction: [dx] }) => {
const trigger = Math.abs(vx) > 0.3 || Math.abs(mx) > 150
if (!active && trigger) {
// Dismiss — fly out
setDismissed(true)
api.start({
x: dx > 0 ? 600 : -600,
rotate: dx * 30,
opacity: 0,
config: { tension: 200, friction: 20 },
onRest: onDismiss,
})
} else {
api.start({
x: active ? mx : 0,
rotate: active ? mx / 12 : 0,
opacity: 1,
immediate: (key) => active && key === "x",
})
}
},
{ filterTaps: true },
)
if (dismissed) return null
return (
<animated.div
{...bind()}
style={{
x,
rotate,
opacity,
touchAction: "none",
cursor: "grab",
userSelect: "none",
}}
className="rounded-2xl border bg-card p-6 shadow-lg w-80"
>
{children}
</animated.div>
)
}
Interpolation and Parallax
// components/animation/ParallaxHero.tsx — scroll-driven parallax
"use client"
import { useSpring, animated } from "@react-spring/web"
import { useEffect } from "react"
export function ParallaxHero({ title, subtitle }: { title: string; subtitle: string }) {
const [{ scrollY }, scrollApi] = useSpring(() => ({ scrollY: 0 }))
useEffect(() => {
const handleScroll = () => scrollApi.start({ scrollY: window.scrollY, immediate: true })
window.addEventListener("scroll", handleScroll, { passive: true })
return () => window.removeEventListener("scroll", handleScroll)
}, [scrollApi])
// Interpolate scroll → parallax transforms
const bgTransform = scrollY.to(y => `translateY(${y * 0.4}px)`)
const headingTransform = scrollY.to(y => `translateY(${y * 0.15}px)`)
const opacity = scrollY.to([0, 400], [1, 0], "clamp")
return (
<div className="relative h-screen overflow-hidden flex items-center justify-center">
{/* Background parallax layer */}
<animated.div
style={{ transform: bgTransform }}
className="absolute inset-0 -top-20 bg-gradient-to-br from-violet-600 via-purple-600 to-blue-700"
/>
{/* Content layer */}
<animated.div
style={{ transform: headingTransform, opacity }}
className="relative z-10 text-center text-white px-6"
>
<h1 className="text-5xl font-bold tracking-tight mb-4">{title}</h1>
<p className="text-xl text-white/80 max-w-2xl mx-auto">{subtitle}</p>
</animated.div>
</div>
)
}
For the Framer Motion alternative when a more feature-complete animation library with layout animations (layout prop), AnimatePresence for exit animations, MotionConfig, variants, and first-class Next.js/App Router support is needed — Framer Motion has more built-in defaults and better documentation for beginners, while react-spring excels at physics-based spring feel and gesture integration, see the Framer Motion guide. For the CSS transitions alternative when simple hover and enter/exit animations without a library are sufficient — Tailwind’s transition-all, duration-300, and ease-in-out utilities handle basic cases with zero bundle cost, see the Tailwind animation guide. The Claude Skills 360 bundle includes react-spring skill sets covering spring animations, transitions, and gesture interactions. Start with the free tier to try physics animation generation.