Rive playback
Play interactive vector animations designed in the Rive editor.
What it is
A .riv file holds artboards (scenes), timeline animations and state machines: the logic that decides which animation plays when, including reactions to the pointer. Designers build all of that in the Rive editor; the app only loads the file. The runtime is WebAssembly, served here from this site (/rive/rive.wasm) rather than a CDN, which is why only the Rive pages' Content Security Policy allows 'wasm-unsafe-eval'.
When to use it
- Interactive icons, buttons and illustrations
- Onboarding and empty-state animations designers can update without code
- Characters and game-like UI that react to input
1. A looping animation
Loading Rive animation…
2. An interactive state machine
Loading Rive animation…
Both files are MIT-licensed examples from Rive's own repositories (see public/rive/CREDITS.md). Files exported from Rive's free plan show a small Rive splash screen.
Show code · src/app/rive-basics/RivePlayer.tsx
"use client";
import "@/lib/rive-runtime";
import { Fit, Layout, useRive } from "@rive-app/react-canvas";
import { useState } from "react";
import { useMotion } from "@/components/motion/MotionProvider";
import { Button, Stage } from "@/components/ui";
type Info = { artboard: string; stateMachines: string[]; animations: string[] };
// Plays a .riv file. Rive files carry their own artboards, timeline
// animations and state machines (the interactive logic); the component lists
// what it found and plays the first state machine, or else the first animation.
export function RivePlayer({ src, label, interactive = false }: { src: string; label: string; interactive?: boolean }) {
const { reduced } = useMotion();
const [info, setInfo] = useState<Info | null>(null);
const [playing, setPlaying] = useState(false);
const { rive, RiveComponent } = useRive(
{
src,
autoplay: false,
layout: new Layout({ fit: Fit.Contain }),
// Called once the file has loaded: inspect it, then start it unless the
// visitor prefers reduced motion. (Interactive files start either way:
// they only move when the visitor points at them.)
onRiveReady: (r) => {
const found = { artboard: r.activeArtboard, stateMachines: r.stateMachineNames, animations: r.animationNames };
setInfo(found);
if (!reduced || interactive) r.play(found.stateMachines[0] ?? found.animations[0]);
},
onPlay: () => setPlaying(true),
onPause: () => setPlaying(false),
onStop: () => setPlaying(false),
},
// Rive pauses rendering by itself while the canvas is off screen.
{ shouldUseIntersectionObserver: true },
);
const toggle = () => {
if (!rive || !info) return;
if (playing) rive.pause();
else rive.play(info.stateMachines[0] ?? info.animations[0]);
};
return (
<figure style={{ margin: 0 }}>
<Stage className="h-65">
<RiveComponent aria-label={label} role="img" />
</Stage>
<div className="mb-2 flex flex-wrap items-center gap-2">
<Button onClick={toggle} disabled={!info}>{playing ? "Pause" : "Play"}</Button>
{interactive && <span className="text-sm text-muted">Hover and click the animation: its state machine reacts to the pointer.</span>}
</div>
{info && (
<figcaption className="text-sm text-muted">
Artboard <code>{info.artboard}</code> · state machines: {info.stateMachines.map((n) => <code key={n}>{n} </code>)}
{info.stateMachines.length === 0 && "none"} · animations: {info.animations.map((n) => <code key={n}>{n} </code>)}
{info.animations.length === 0 && "none"}
</figcaption>
)}
</figure>
);
}
Show code · src/lib/rive-runtime.ts
import { RuntimeLoader } from "@rive-app/react-canvas";
// Load Rive's WebAssembly runtime from this site (public/rive/rive.wasm,
// copied there by scripts/copy-rive-wasm.mjs) instead of the jsDelivr CDN
// Rive uses by default, and turn the CDN fallback off. Import this module
// before any Rive component renders.
RuntimeLoader.setWasmUrl("/rive/rive.wasm");
RuntimeLoader.setWasmFallbackUrl(null);