Lottie vs Rive vs CSS Animations: Web Animation Formats (2026)
TL;DR
Lottie plays After Effects animations on the web — designers export from AE with the Bodymovin plugin, developers render with lottie-web or @lottiefiles/dotlottie-web, vector animations, widely adopted. Rive is the interactive animation runtime — state machines, blend states, mesh deformation, GPU-accelerated, real-time interaction, designed for developers. CSS Animations are browser-native — keyframes, transitions, GPU-composited, no dependencies, best for UI micro-interactions. In 2026: Lottie for designer-created animations (icons, illustrations), Rive for interactive stateful animations, CSS animations for UI transitions and micro-interactions.
Key Takeaways
- Lottie: lottie-web ~300K weekly downloads — After Effects export, vector, JSON/.lottie
- Rive: @rive-app/canvas ~50K weekly downloads — state machines, GPU, interactive
- CSS Animations: Built into browsers — no dependencies, GPU-composited, performant
- Lottie is the industry standard for designer → developer animation handoff
- Rive enables interactivity without code (state machines in the editor)
- CSS animations are the most performant for simple UI animations
Lottie
Lottie — After Effects animations for web:
Basic player
<!-- dotLottie player (modern): -->
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@latest/dist/dotlottie-wc.js"
type="module"></script>
<dotlottie-wc
src="https://lottie.host/animation.lottie"
autoplay
loop
style="width: 300px; height: 300px">
</dotlottie-wc>
React integration
import { DotLottieReact } from "@lottiefiles/dotlottie-react"
import { useState } from "react"
function AnimatedIcon() {
return (
<DotLottieReact
src="/animations/success-check.lottie"
autoplay
loop={false}
style={{ width: 120, height: 120 }}
/>
)
}
// With controls:
function ControlledAnimation() {
const [dotLottie, setDotLottie] = useState(null)
return (
<div>
<DotLottieReact
src="/animations/loading.lottie"
loop
autoplay
dotLottieRefCallback={setDotLottie}
/>
<button onClick={() => dotLottie?.play()}>Play</button>
<button onClick={() => dotLottie?.pause()}>Pause</button>
<button onClick={() => dotLottie?.stop()}>Stop</button>
<button onClick={() => dotLottie?.setSpeed(2)}>2x Speed</button>
</div>
)
}
lottie-web (classic)
import lottie from "lottie-web"
// Load animation:
const anim = lottie.loadAnimation({
container: document.getElementById("animation")!,
renderer: "svg", // "svg" | "canvas" | "html"
loop: true,
autoplay: true,
path: "/animations/hero.json", // JSON animation file
})
// Controls:
anim.play()
anim.pause()
anim.stop()
anim.setSpeed(1.5)
anim.setDirection(-1) // Reverse
anim.goToAndStop(30, true) // Go to frame 30
// Events:
anim.addEventListener("complete", () => {
console.log("Animation complete")
})
anim.addEventListener("loopComplete", () => {
console.log("Loop finished")
})
// Destroy:
anim.destroy()
Interactivity (scroll-based)
import { DotLottieReact } from "@lottiefiles/dotlottie-react"
import { useEffect, useRef, useState } from "react"
function ScrollAnimation() {
const [dotLottie, setDotLottie] = useState(null)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!dotLottie || !containerRef.current) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
dotLottie.play()
} else {
dotLottie.pause()
}
},
{ threshold: 0.5 }
)
observer.observe(containerRef.current)
return () => observer.disconnect()
}, [dotLottie])
return (
<div ref={containerRef}>
<DotLottieReact
src="/animations/scroll-reveal.lottie"
loop={false}
dotLottieRefCallback={setDotLottie}
/>
</div>
)
}
Rive
Rive — interactive animation runtime:
Basic setup
<!-- Rive canvas: -->
<script src="https://unpkg.com/@rive-app/canvas@latest"></script>
<canvas id="rive-canvas" width="500" height="500"></canvas>
<script>
const rive = new RiveCanvas.Rive({
src: "/animations/character.riv",
canvas: document.getElementById("rive-canvas"),
autoplay: true,
stateMachines: "main", // State machine name
onLoad: () => {
rive.resizeDrawingSurfaceToCanvas()
},
})
</script>
React integration
import { useRive, useStateMachineInput } from "@rive-app/react-canvas"
function InteractiveButton() {
const { rive, RiveComponent } = useRive({
src: "/animations/button.riv",
stateMachines: "button_state",
autoplay: true,
})
// Connect to state machine inputs:
const hoverInput = useStateMachineInput(rive, "button_state", "isHovered")
const clickInput = useStateMachineInput(rive, "button_state", "isPressed")
return (
<RiveComponent
style={{ width: 200, height: 60 }}
onMouseEnter={() => hoverInput && (hoverInput.value = true)}
onMouseLeave={() => hoverInput && (hoverInput.value = false)}
onMouseDown={() => clickInput?.fire()}
/>
)
}
State machines
import { useRive, useStateMachineInput } from "@rive-app/react-canvas"
// Interactive toggle with state machine:
function AnimatedToggle({ onChange }: { onChange: (on: boolean) => void }) {
const { rive, RiveComponent } = useRive({
src: "/animations/toggle.riv",
stateMachines: "toggle_sm",
autoplay: true,
})
const isOn = useStateMachineInput(rive, "toggle_sm", "isOn")
const handleClick = () => {
if (isOn) {
isOn.value = !isOn.value
onChange(isOn.value)
}
}
return (
<RiveComponent
onClick={handleClick}
style={{ width: 80, height: 40, cursor: "pointer" }}
/>
)
}
// Character with multiple states:
function GameCharacter() {
const { rive, RiveComponent } = useRive({
src: "/animations/character.riv",
stateMachines: "character_sm",
autoplay: true,
})
const speed = useStateMachineInput(rive, "character_sm", "speed")
const isJumping = useStateMachineInput(rive, "character_sm", "jump")
const health = useStateMachineInput(rive, "character_sm", "health")
return (
<div>
<RiveComponent style={{ width: 300, height: 300 }} />
<input
type="range" min="0" max="100"
onChange={(e) => speed && (speed.value = +e.target.value)}
/>
<button onClick={() => isJumping?.fire()}>Jump</button>
<input
type="range" min="0" max="100"
onChange={(e) => health && (health.value = +e.target.value)}
/>
</div>
)
}
Events and callbacks
import { useRive, EventType } from "@rive-app/react-canvas"
function AnimationWithEvents() {
const { RiveComponent } = useRive({
src: "/animations/onboarding.riv",
stateMachines: "flow",
autoplay: true,
onStateChange: (event) => {
console.log("State changed:", event.data)
},
})
return <RiveComponent style={{ width: 400, height: 400 }} />
}
CSS Animations
Browser-native — keyframes and transitions:
Keyframe animations
/* Fade in: */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
/* Pulse: */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}
/* Spin: */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
}
Transitions
/* Smooth hover: */
.button {
background: #3b82f6;
color: white;
padding: 12px 24px;
border-radius: 6px;
transition: all 0.2s ease;
}
.button:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
/* Accordion expand: */
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-content.open {
max-height: 500px;
}
/* Slide in sidebar: */
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
React with CSS animations
import { useState } from "react"
import styles from "./Toast.module.css"
function Toast({ message, onClose }: { message: string; onClose: () => void }) {
const [exiting, setExiting] = useState(false)
const handleClose = () => {
setExiting(true)
setTimeout(onClose, 300) // Match animation duration
}
return (
<div className={`${styles.toast} ${exiting ? styles.exit : styles.enter}`}>
<span>{message}</span>
<button onClick={handleClose}>×</button>
</div>
)
}
/* Toast.module.css */
/*
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #1a1a1a;
color: white;
padding: 12px 20px;
border-radius: 8px;
}
.enter {
animation: slideIn 0.3s ease-out forwards;
}
.exit {
animation: slideOut 0.3s ease-in forwards;
}
@keyframes slideIn {
from { transform: translateX(120%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(120%); opacity: 0; }
}
*/
Modern CSS features
/* View Transitions API (Chrome 111+): */
::view-transition-old(root) {
animation: fade-out 0.3s ease;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease;
}
/* Scroll-driven animations (Chrome 115+): */
@keyframes reveal {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
.scroll-reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* Container queries + animation: */
@container (min-width: 600px) {
.card {
animation: expandCard 0.3s ease forwards;
}
}
Feature Comparison
| Feature | Lottie | Rive | CSS Animations |
|---|---|---|---|
| Source | After Effects | Rive editor | Code |
| File format | JSON / .lottie | .riv (binary) | CSS/JS |
| Renderer | SVG / Canvas | Canvas (WASM) | Browser |
| Bundle size | ~50KB (dotlottie) | ~200KB (WASM) | 0KB |
| State machines | ❌ | ✅ | ❌ |
| Interactivity | Basic (play/pause) | ✅ (inputs, events) | Hover/focus/active |
| Mesh deformation | ❌ | ✅ | ❌ |
| Design tool | After Effects | Rive (free) | DevTools |
| Vector | ✅ | ✅ | N/A |
| Performance | Good | Excellent (GPU) | Excellent (composited) |
| React bindings | ✅ | ✅ | Native (className) |
| Learning curve | Low (devs) | Medium | Low |
| Designer handoff | ✅ (Bodymovin) | ✅ (Rive editor) | ❌ (manual) |
| Adoption | Industry standard | Growing | Universal |
When to Use Each
Use Lottie if:
- Designers create animations in After Effects
- Need vector animations (icons, illustrations, onboarding)
- Want the industry-standard designer → developer handoff
- Building marketing pages or app micro-interactions
Use Rive if:
- Need interactive animations with state machines
- Want GPU-accelerated rendering for complex animations
- Building game-like UI with blend states and mesh deformation
- Need real-time user interaction (hover, click, drag affects animation)
Use CSS Animations if:
- Building UI transitions (hover, focus, enter/exit)
- Want zero-dependency, maximum performance
- Need simple animations (fade, slide, scale, rotate)
- Performance is critical (GPU-composited transforms)
Methodology
Feature comparison based on @lottiefiles/dotlottie-react v0.x, @rive-app/react-canvas v4.x, and CSS Animations Level 2 specification as of March 2026.