Rive Animation in React — State Machines, Hover Triggers & More
How to use Rive animations in React with @rive-app/react-canvas. Covers useRive hook, state machine inputs, hover and click triggers, and common pitfalls.
Rive is a real-time animation format built for apps and games. Unlike Lottie (which plays a pre-baked timeline), Rive animations have state machines — graph-based logic that responds to inputs like booleans, numbers, and triggers. This makes Rive ideal for interactive icons: hover states, click animations, loading-to-success transitions.
This guide covers the @rive-app/react-canvas package for React.
Download .riv files at Unicorn Icons — every icon ships with a native Rive file.
How Rive state machines work
A Rive file (.riv) contains one or more artboards, each with optional state machines. State machines have:
- States — animation clips (e.g., "idle", "hover", "active")
- Transitions — rules for moving between states
- Inputs — external values that trigger transitions:
Boolean— true/false (e.g., "is hovered")Number— numeric values (e.g., "progress")Trigger— one-shot event (e.g., "clicked")
In React, you read and set these inputs via hooks. The runtime handles the state transitions and rendering.
All Unicorn Icons .riv files include a "State Machine 1" with a Hover boolean input by default.
Setup
1. Install
bashcopynpm install @rive-app/react-canvas # or yarn add @rive-app/react-canvas
2. Basic usage — looping animation
tsxcopy"use client"; import { useRive } from "@rive-app/react-canvas"; export default function ArrowIcon() { const { RiveComponent } = useRive({ src: "/icons/arrow.riv", autoplay: true, }); return <RiveComponent style={{ width: 32, height: 32 }} />; }
RiveComponent is a canvas element. Always set an explicit size — the canvas needs dimensions.
3. With a state machine
tsxcopy"use client"; import { useRive, Layout, Fit, Alignment } from "@rive-app/react-canvas"; export default function ArrowIcon() { const { RiveComponent } = useRive({ src: "/icons/arrow.riv", stateMachines: "State Machine 1", autoplay: true, layout: new Layout({ fit: Fit.Contain, alignment: Alignment.Center, }), }); return <RiveComponent style={{ width: 32, height: 32 }} />; }
Layout controls how the animation fits the canvas. Fit.Contain (default) scales to fit without cropping.
Interactive inputs
Boolean input — hover trigger
tsxcopy"use client"; import { useRive, useStateMachineInput } from "@rive-app/react-canvas"; export default function HoverIcon() { const { RiveComponent, rive } = useRive({ src: "/icons/arrow.riv", stateMachines: "State Machine 1", autoplay: true, }); const hoverInput = useStateMachineInput(rive, "State Machine 1", "Hover"); return ( <button onMouseEnter={() => { if (hoverInput) hoverInput.value = true; }} onMouseLeave={() => { if (hoverInput) hoverInput.value = false; }} style={{ background: "none", border: "none", padding: 0, cursor: "pointer" }} > <RiveComponent style={{ width: 32, height: 32 }} /> </button> ); }
useStateMachineInput returns null until the Rive runtime loads — always guard with if (hoverInput).
Trigger input — click animation
tsxcopy"use client"; import { useRive, useStateMachineInput } from "@rive-app/react-canvas"; export default function ClickIcon() { const { RiveComponent, rive } = useRive({ src: "/icons/check.riv", stateMachines: "State Machine 1", autoplay: true, }); const clickTrigger = useStateMachineInput(rive, "State Machine 1", "Click"); return ( <button onClick={() => clickTrigger?.fire()}> <RiveComponent style={{ width: 32, height: 32 }} /> </button> ); }
Trigger inputs expose .fire() instead of .value.
Number input — progress
tsxcopyconst progressInput = useStateMachineInput(rive, "State Machine 1", "Progress"); // Set to a value between 0 and 100 progressInput.value = 75;
Reusable RiveIcon component
tsxcopy"use client"; import { useRive, useStateMachineInput, Layout, Fit, Alignment } from "@rive-app/react-canvas"; interface RiveIconProps { src: string; size?: number; stateMachine?: string; hoverInput?: string; className?: string; } export default function RiveIcon({ src, size = 32, stateMachine = "State Machine 1", hoverInput = "Hover", className, }: RiveIconProps) { const { RiveComponent, rive } = useRive({ src, stateMachines: stateMachine, autoplay: true, layout: new Layout({ fit: Fit.Contain, alignment: Alignment.Center }), }); const hover = useStateMachineInput(rive, stateMachine, hoverInput); return ( <div className={className} style={{ width: size, height: size, cursor: "pointer" }} onMouseEnter={() => { if (hover) hover.value = true; }} onMouseLeave={() => { if (hover) hover.value = false; }} > <RiveComponent /> </div> ); }
Usage:
tsxcopy<RiveIcon src="/icons/arrow.riv" size={24} /> <RiveIcon src="/icons/check.riv" size={32} />
Common pitfalls
Pitfall 1: rive is null on first render
rive from useRive is null until the file loads. useStateMachineInput returns null in this state. Always use optional chaining or null-check before accessing inputs.
tsxcopy// ❌ Crashes on mount hoverInput.value = true; // ✅ if (hoverInput) hoverInput.value = true; // or hoverInput?.value = true; // Note: optional chaining on assignment isn't valid TS
Pitfall 2: Mismatched state machine or input name
The strings "State Machine 1" and "Hover" must exactly match what's defined in the Rive file. Check these in the Rive editor. Unicorn Icons files use "State Machine 1" / "Hover" consistently.
Pitfall 3: SSR in Next.js
@rive-app/react-canvas uses canvas — it can't render server-side. Add "use client" to any component that uses it, or use dynamic():
tsxcopyimport dynamic from "next/dynamic"; const RiveIcon = dynamic(() => import("@/components/RiveIcon"), { ssr: false });
See the Next.js animated icons guide.
Pitfall 4: Canvas not filling its container
The Rive canvas may not fill the parent div. Fix with explicit dimensions on both the wrapper and RiveComponent:
tsxcopy<div style={{ width: 32, height: 32 }}> <RiveComponent style={{ width: "100%", height: "100%" }} /> </div>
Rive vs Lottie — when to use each
| Scenario | Rive | Lottie |
|---|---|---|
| Hover/click interactive icon | ✓ Best | Limited |
| Simple looping icon | ✓ | ✓ |
| File size matters | ✓ Smaller | Larger |
| After Effects workflow | — | ✓ |
| iOS / Android native | ✓ | ✓ |
| Browser support | All modern | All modern |
For a deep comparison, see Rive vs Lottie — which format to choose.