Skip to content

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 .riv file hosted at a URL or in public/.
  • The state machine name from handoff.md.
  • React 18+.

Install

bash
npm install @rive-app/react-webgl2

Paste this

tsx
"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.

tsx
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:

tsx
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) when handoff.md requires features only WebGL2 supports (mesh, vector feathering, advanced blend modes).
  • Reading rive before it is ready. rive from useRive is null on first render — guard with if (!rive) return; inside effects.
  • Reusing the same input across mounts without remounting. If the .riv changes, remount the component so useStateMachineInput re-binds.
  • No aspectRatio on 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.

Developer support for Lupian-delivered Rive animations