React / Next.js
Same .riv file, same artboard, state machine, input, and event names as Vanilla Web. Only the wrapper code differs.
Use this when
You are integrating into a React app (CRA, Vite, Remix, Gatsby) or a Next.js app.
What you need
- The
.rivfile hosted at a URL or inpublic/. - The state machine name from
handoff.md. - React 18+.
Install
npm install @rive-app/react-webgl2Paste this
"use client"; // Next.js App Router only
import { useRive, Layout, Fit, Alignment } from "@rive-app/react-webgl2";
export function LupianHero() {
const { RiveComponent } = useRive({
src: "/animations/hero.riv", // from handoff
stateMachines: "State Machine 1", // from handoff.md
autoplay: true,
layout: new Layout({
fit: Fit.Contain,
alignment: Alignment.Center,
}),
});
return (
<div style={{ width: "100%", aspectRatio: "16 / 9" }}>
<RiveComponent style={{ width: "100%", height: "100%" }} />
</div>
);
}useRive handles canvas creation, sizing, and cleanup. You do not need to call resizeDrawingSurfaceToCanvas() or cleanup() yourself.
SSR / Next.js warning
Rive is browser-only
The Rive runtime needs window, document, WebGL, and WASM. It cannot render on the server.
- App Router: mark the component file with
"use client". - Pages Router or any SSR: dynamic-import the component with SSR disabled:tsx
import dynamic from "next/dynamic"; const LupianHero = dynamic(() => import("./LupianHero"), { ssr: false });
Wiring inputs and events
Same names as Vanilla Web — useStateMachineInput is the React-friendly accessor.
import {
useRive,
useStateMachineInput,
} from "@rive-app/react-webgl2";
export function InteractiveButton() {
const { rive, RiveComponent } = useRive({
src: "/animations/button.riv",
stateMachines: "Button SM",
autoplay: true,
});
const isHover = useStateMachineInput(rive, "Button SM", "isHovering");
const onClick = useStateMachineInput(rive, "Button SM", "onClick");
return (
<RiveComponent
style={{ width: 200, height: 60 }}
onMouseEnter={() => isHover && (isHover.value = true)}
onMouseLeave={() => isHover && (isHover.value = false)}
onClick={() => onClick?.fire()}
/>
);
}For event listening (custom events fired from the animation), see State machine inputs & events.
The same names rule
The .riv file does not care which runtime you use. The names in handoff.md — artboard, state machine, inputs, events — are identical for Vanilla Web, React, and Framer. If you ever switch wrappers, the names stay; only the surrounding code changes.
Multiple animations on one page
Browsers cap WebGL contexts (8–16). Enable the offscreen renderer on every instance:
useRive({
src: "/animations/card.riv",
stateMachines: "Idle",
autoplay: true,
useOffscreenRenderer: true,
});See Multiple animations on a page.
Common mistakes
- Forgetting
"use client"(Next.js App Router) — produces a hydration or import error at build time. - Importing the React package at the top level of a server component — same fix.
- Using
@rive-app/react(Canvas2D wrapper) whenhandoff.mdrequires features only WebGL2 supports (mesh, vector feathering, advanced blend modes). - Reading
rivebefore it is ready.rivefromuseRiveisnullon first render — guard withif (!rive) return;inside effects. - Reusing the same input across mounts without remounting. If the
.rivchanges, remount the component souseStateMachineInputre-binds. - No
aspectRatioon the container. The canvas needs a sized parent — otherwise it can mount at 0×0.
If stuck, send this to Lupian
- The repo or a minimal reproduction (CodeSandbox / StackBlitz works).
- The framework (Next.js App Router / Pages Router / Vite / etc.) and React version.
- Console + network tab screenshots.
- See Ask Lupian for help.

