Lottie renders After Effects animations as JSON in the browser — lottie-react wraps lottie-web with a <Lottie> component and animationData prop for bundled JSON. lottieRef.current.play(), .pause(), .stop(), .setSpeed(2), and .goToAndPlay(frame, true) control playback imperatively. loop={false} plays once; autoplay={false} waits for .play(). onComplete and onLoopComplete are event callbacks. DotLottie is the newer .lottie binary format (50% smaller than JSON) via @lottiefiles/dotlottie-react. Segment playback with playSegments([[0, 30], [60, 90]]) plays named states. lottie.loadAnimation({ container, renderer: "svg", animationData }) is the vanilla API without React. lottie-react-native uses the same JSON files for mobile. Claude Code generates Lottie loading skeletons, success/error state animations, button micro-interactions, and onboarding illustrations.
CLAUDE.md for Lottie
## Lottie Stack
- Version: lottie-react >= 2.4, @lottiefiles/dotlottie-react >= 0.12
- JSON: import animationData from "@/animations/success.json" — bundled
- URL: animationData fetched at runtime for large files
- Ref: const lottieRef = useRef<LottieRefCurrentProps>(null)
- Controls: lottieRef.current.play() | .pause() | .stop() | .setSpeed(1.5)
- Events: onComplete, onLoopComplete, onEnterFrame({ currentTime, totalTime })
- DotLottie: <DotLottieReact src="/animations/confetti.lottie" loop autoplay />
- Segments: lottieRef.current.playSegments([startFrame, endFrame], true)
Lottie React Component
// components/animation/LottiePlayer.tsx — reusable Lottie component
"use client"
import Lottie, { type LottieRefCurrentProps } from "lottie-react"
import { useRef, useEffect, useCallback, forwardRef } from "react"
interface LottiePlayerProps {
animationData: object
width?: number | string
height?: number | string
loop?: boolean
autoplay?: boolean
speed?: number
className?: string
onComplete?: () => void
onLoopComplete?: () => void
style?: React.CSSProperties
}
export interface LottiePlayerRef {
play: () => void
pause: () => void
stop: () => void
setSpeed: (speed: number) => void
goToAndPlay: (frame: number) => void
goToAndStop: (frame: number) => void
}
export const LottiePlayer = forwardRef<LottiePlayerRef, LottiePlayerProps>(
function LottiePlayer(
{ animationData, width = "100%", height = "auto", loop = true, autoplay = true, speed = 1, className, onComplete, onLoopComplete, style },
ref,
) {
const lottieRef = useRef<LottieRefCurrentProps>(null)
// Expose imperative controls via forwarded ref
useEffect(() => {
if (!ref || typeof ref !== "object" || !ref.current) return
if (!lottieRef.current) return
const controls = lottieRef.current
ref.current.play = () => controls.play()
ref.current.pause = () => controls.pause()
ref.current.stop = () => controls.stop()
ref.current.setSpeed = (s) => controls.setSpeed(s)
ref.current.goToAndPlay = (frame) => controls.goToAndPlay(frame, true)
ref.current.goToAndStop = (frame) => controls.goToAndStop(frame, true)
}, [ref])
useEffect(() => {
lottieRef.current?.setSpeed(speed)
}, [speed])
return (
<Lottie
lottieRef={lottieRef}
animationData={animationData}
loop={loop}
autoplay={autoplay}
onComplete={onComplete}
onLoopComplete={onLoopComplete}
style={{ width, height, ...style }}
className={className}
/>
)
},
)
State Machine Animations
// components/animation/StatusAnimation.tsx — multi-state Lottie
"use client"
import Lottie, { type LottieRefCurrentProps } from "lottie-react"
import { useRef, useEffect, useState } from "react"
// Import animation data
import loadingAnimation from "@/animations/loading-spinner.json"
import successAnimation from "@/animations/success-checkmark.json"
import errorAnimation from "@/animations/error-shake.json"
type AnimationState = "idle" | "loading" | "success" | "error"
const ANIMATIONS: Record<Exclude<AnimationState, "idle">, { data: object; loop: boolean }> = {
loading: { data: loadingAnimation, loop: true },
success: { data: successAnimation, loop: false },
error: { data: errorAnimation, loop: false },
}
// Segment ranges within a single "state machine" Lottie file
const SEGMENTS = {
idle: [0, 0] as [number, number],
loading: [0, 60] as [number, number],
success: [60, 120] as [number, number],
error: [120, 180] as [number, number],
}
interface StatusAnimationProps {
state: AnimationState
size?: number
onComplete?: () => void
}
export function StatusAnimation({ state, size = 80, onComplete }: StatusAnimationProps) {
if (state === "idle") return <div style={{ width: size, height: size }} />
const { data, loop } = ANIMATIONS[state]
return (
<Lottie
animationData={data}
loop={loop}
autoplay={true}
onComplete={onComplete}
style={{ width: size, height: size }}
/>
)
}
// Button with embedded loading/success animation
interface AnimatedButtonProps {
onClick: () => Promise<void>
children: React.ReactNode
className?: string
}
export function AnimatedSubmitButton({ onClick, children, className }: AnimatedButtonProps) {
const [state, setState] = useState<AnimationState>("idle")
const handleClick = async () => {
if (state !== "idle") return
setState("loading")
try {
await onClick()
setState("success")
setTimeout(() => setState("idle"), 2000)
} catch {
setState("error")
setTimeout(() => setState("idle"), 2500)
}
}
const isIdle = state === "idle"
return (
<button
onClick={handleClick}
disabled={state !== "idle"}
className={`relative flex items-center justify-center gap-2 ${className}`}
>
{!isIdle && (
<StatusAnimation
state={state}
size={24}
onComplete={state === "success" || state === "error" ? undefined : undefined}
/>
)}
<span className={state !== "idle" ? "sr-only" : ""}>{children}</span>
{state === "loading" && <span>Processing...</span>}
{state === "success" && <span>Done!</span>}
{state === "error" && <span>Try again</span>}
</button>
)
}
DotLottie Player
// components/animation/DotLottieHero.tsx — .lottie binary format (smaller)
"use client"
import { DotLottieReact, type DotLottieRefProps } from "@lottiefiles/dotlottie-react"
import { useRef, useCallback } from "react"
interface HeroAnimationProps {
src: string
className?: string
}
export function HeroAnimation({ src, className }: HeroAnimationProps) {
const dotLottieRef = useRef<DotLottieRefProps>(null)
const handleMouseEnter = useCallback(() => {
dotLottieRef.current?.dotLottie?.setSpeed(1.5)
}, [])
const handleMouseLeave = useCallback(() => {
dotLottieRef.current?.dotLottie?.setSpeed(1)
}, [])
return (
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={className}
>
<DotLottieReact
ref={dotLottieRef}
src={src}
loop
autoplay
renderConfig={{ autoResize: true }}
/>
</div>
)
}
Lazy-Loaded Remote Animation
// components/animation/RemoteLottie.tsx — fetch JSON from CDN, lazy
"use client"
import { useState, useEffect } from "react"
import dynamic from "next/dynamic"
// Lazy-import lottie-react to avoid SSR issues
const Lottie = dynamic(() => import("lottie-react"), {
ssr: false,
loading: () => <div className="animate-pulse bg-muted rounded-full" />,
})
interface RemoteLottieProps {
url: string
width?: number
height?: number
loop?: boolean
}
export function RemoteLottie({ url, width = 200, height = 200, loop = true }: RemoteLottieProps) {
const [animationData, setAnimationData] = useState<object | null>(null)
useEffect(() => {
let cancelled = false
fetch(url)
.then(r => r.json())
.then(data => { if (!cancelled) setAnimationData(data) })
.catch(console.error)
return () => { cancelled = true }
}, [url])
if (!animationData) {
return (
<div
className="animate-pulse bg-muted rounded-full"
style={{ width, height }}
/>
)
}
return (
<Lottie
animationData={animationData}
loop={loop}
autoplay
style={{ width, height }}
/>
)
}
For the react-spring alternative when physics-based spring animations driven by JavaScript state rather than pre-exported After Effects files are preferred — react-spring is code-authored and great for gesture-driven interactions, while Lottie plays designer-authored animations that require no coding but also cannot be dynamically driven by data, see the react-spring guide. For the CSS/Tailwind animation alternative when simple keyframe animations (bounce, pulse, spin, fade) without a library are sufficient — Tailwind’s animate-spin, animate-pulse, and custom @keyframes in tailwind.config handle icon spinners and skeleton loaders at zero bundle cost, see the Tailwind animation guide. The Claude Skills 360 bundle includes Lottie skill sets covering animation players, state machines, and DotLottie integration. Start with the free tier to try Lottie animation generation.