Bringing new tools into a workflow is always exciting—curiosity bumps up against the comfort of familiar methods. But when our longtime client, Chumbi Valley, came to us with their Valley Adventures project, we saw the perfect opportunity to experiment with Rive and craft cartoon-style animations that matched the playful spirit of the brand.
Rive is a powerful real-time interactive design tool with built-in support for interactivity through State Machines. In this guide, we’ll walk you through how we integrated a .riv file into a React environment and added mouse-responsive animations.
We’ll also walk through a modernized integration method using Rive’s newer Data Binding feature—our current preferred approach for achieving the same animation with less complexity and greater flexibility.
Animation Concept & File Preparation
Valley Adventures is a gamified Chumbi NFT staking program, where magical creatures called Chumbi inhabit an enchanted world. The visual direction leans heavily into fairytale book illustrations—vibrant colors, playful characters, and a whimsical, cartoon-like aesthetic.

To immediately immerse users in this world, we went with a full-section hero animation on the landing page. We split the animation into two parts:
- an idle animation that brings the scene to life;
- a cursor-triggered parallax effect, adding depth and interactivity.
Several elements animate simultaneously—background layers like rustling leaves and flickering fireflies, along with foreground characters that react to movement. The result is a dynamic, storybook-like experience that invites users to explore.
The most interesting—and trickiest—part of the integration was tying animations to mouse tracking. Rive provides a built-in way to handle this: by applying constraints with varying strengths to elements within a group that’s linked to Mouse Tracking, which itself responds to the cursor’s position.
However, we encountered a limitation with this approach: the HTML buttons layered above the Rive asset were blocking the hover state, preventing it from triggering the animation beneath.
To work around this, we used a more robust method that gave us finer control and avoided those problems altogether.
Here’s how we approached it:
- Create four separate timelines, each with a single keyframe representing an extreme position of the animation group:
- Far left
- Far right
- Top
- Bottom
- Add two animation layers, each responsible for blending between opposite keyframes:
- Layer 1 blends the far-left and far-right timelines
- Layer 2 blends the top and bottom timelines
- Tie each layer’s blend amount to a numeric input—one for the X axis, one for the Y axis.
By adjusting the values of these inputs based on the cursor’s position, you can control how tightly the animation responds on each axis. This approach gives you a smoother, more customizable parallax effect—and prevents unexpected behavior caused by overlapping UI.
Once the animation is ready, simply export it as a .riv file—and leave the rest of the magic to the devs.
How We Did It: Integrating a Rive File into a React Project
Before we dive further, let’s clarify what a .riv file actually is.
A .riv file is the export format from the Rive editor. It can include:
- vector graphics,
- timeline animations,
- a State Machine with input parameters.
In our case, we’re using a State Machine with two numeric inputs: Axis_X and Axis_Y. These inputs are tied to how we control animation in Rive, using values from the X and Y axes of the cursor’s position.
These inputs drive the movement of different elements—like the swaying leaves, fluttering fireflies, and even subtle character reactions—creating a smooth, interactive experience that responds to the user’s mouse.
Step-by-Step Integration
Step 1: Install the Rive React runtime
Install the official package:
npm install @rive-app/react-canvas
Step 2: Create an Animation Component
Create a component called RiveBackground.tsx to handle loading and rendering the animation.
Step 3: Connect animation
const { rive, setCanvasRef, setContainerRef } = useRive({
src: 'https://cdn.rive.app/animations/hero.riv',
autoplay: true,
layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }),
onLoad: () => setIsLoaded(true),
enableRiveAssetCDN: true,
});
For a better understanding, let’s take a closer look at each prop you’ll typically use when working with Rive in React:
What each option does:
Property | Description |
src | Path to your .riv file — can be local or hosted via CDN |
autoplay | Automatically starts the animation once it’s loaded |
layout | Controls how the animation fits into the canvas (we’re using Cover and Center) |
onLoad | Callback that fires when the animation is ready — useful for setting isLoaded |
enableRiveAssetCDN | Allows loading of external assets (like fonts or textures) from Rive’s CDN |
Step 4: Connect State Machine Inputs
const numX = useStateMachineInput(rive, 'State Machine 1', 'Axis_X', 0);
const numY = useStateMachineInput(rive, 'State Machine 1', 'Axis_Y', 0);
This setup connects directly to the input values defined inside the State Machine, allowing us to update them dynamically in response to user interaction.
- State Machine 1 — the name of your State Machine, exactly as defined in the Rive editor
- Axis_X and Axis_Y — numeric inputs that control movement based on cursor position
- 0 — the initial (default) value for each input
☝️ Important: Make sure your .riv file includes the exact names: Axis_X, Axis_Y, and State Machine 1. These must match what’s defined in the Rive editor — otherwise, the animation won’t respond as expected.
Step 5: Handle Mouse Movement
useEffect(() => {
if (!numX || !numY) return;
const handleMouseMove = (e: MouseEvent) => {
const { innerWidth, innerHeight } = window;
numX.value = (e.clientX / innerWidth) * 100;
numY.value = 100 - (e.clientY / innerHeight) * 100;
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [numX, numY]);
What’s happening here:
- We use
clientX
andclientY
to track the mouse position within the browser window. - The values are normalized to a 0–100 range, matching what the animation expects.
- These normalized values are then passed to the Axis_X and Axis_Y inputs in the Rive State Machine, driving the interactive animation.
⚠️ Important: Always remember to remove the event listener when the component unmounts to avoid memory leaks and unwanted behavior.
Step 6: Cleanup and Render the Component
useEffect(() => {
return () => rive?.cleanup();
}, [rive]);
And the render:
return (
<div
ref={setContainerRef}
className={`rive-container ${className ?? ''} ${isLoaded ? 'show' : 'hide'}`}
>
<canvas ref={setCanvasRef} />
</div>
);
cleanup()
— frees up resources when the component unmounts. Always call this to prevent memory leaks.setCanvasRef
andsetContainerRef
— these must be connected to the correct DOM elements in order for Rive to render the animation properly.
And here’s the complete code:
import {
useRive,
useStateMachineInput,
Layout,
Fit,
Alignment,
} from '@rive-app/react-canvas';
import { useEffect, useState } from 'react';
export function RiveBackground({ className }: { className?: string }) {
const [isLoaded, setIsLoaded] = useState(false);
const { rive, setCanvasRef, setContainerRef } = useRive({
src: 'https://cdn.rive.app/animations/hero.riv',
animations: ['State Machine 1','Timeline 1','Timeline 2'
],
autoplay: true,
layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }),
onLoad: () => setIsLoaded(true),
enableRiveAssetCDN: true,
});
const numX = useStateMachineInput(rive, 'State Machine 1', 'Axis_X', 0);
const numY = useStateMachineInput(rive, 'State Machine 1', 'Axis_Y', 0);
useEffect(() => {
if (!numX || !numY) return;
const handleMouseMove = (e: MouseEvent) => {
if (!numX || !numY) {
return;
}
const { innerWidth, innerHeight } = window;
numX.value = (e.clientX / innerWidth) * 100;
numY.value = 100 - (e.clientY / innerHeight) * 100;
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [numX, numY]);
useEffect(() => {
return () => {
rive?.cleanup();
};
}, [rive]);
return (
<div
ref={setContainerRef}
className={`rive-container ${className ?? ''} ${isLoaded ? 'show' : 'hide'}`}
>
<canvas ref={setCanvasRef} />
</div>
);
}
Step 7: Use the Component
Now you can use the RiveBackground
like any other component:
<RiveBackground className="hero-background" />
Step 8: Preload the WASM File
To avoid loading the .wasm file at runtime—which can delay the initial render—you can preload it in App.tsx:
import riveWASMResource from '@rive-app/canvas/rive.wasm';
<link
rel="preload"
href={riveWASMResource}
as="fetch"
crossOrigin="anonymous"
/>
This is especially useful if you’re optimizing for first paint or overall performance.
Simple Parallax: A New Approach with Data Binding
In the first part of this article, we used a classic approach with a State Machine to create the parallax animation in Rive. We built four separate animations (top, bottom, left, right), controlled them using input variables, and blended their states to create smooth motion. This method made sense at the time, especially before Data Binding support was introduced.
But now that Data Binding is available in Rive, achieving the same effect is much simpler—just a few steps. Data binding in Rive is a system that connects editor elements to dynamic data and code via view models, enabling reactive, runtime-driven updates and interactions between design and development.
In this section, we’ll show how to refactor the original Rive file and code using the new approach.
Updating the Rive File
- Remove the old setup:
- Go to the State Machine.
- Delete the input variables: top, bottom, left, right.
- Remove the blending states and their associated animations.
- Group the parallax layers:
- Wrap all the parallax layers into a new group—e.g., ParallaxGroup.
- Create binding parameters:
- Select ParallaxGroup and add:
- pointerX (Number)
- pointerY (Number)
- Select ParallaxGroup and add:
- Bind coordinates:
- In the properties panel, set:
- X → pointerX
- Y → pointerY
- In the properties panel, set:
Now the group will move dynamically based on values passed from JavaScript.
The Updated JS Code
Before we dive into the updated JavaScript, let’s quickly define an important concept:
When using Data Binding in Rive, viewModelInstance
refers to the runtime object that links your Rive file’s bindable properties (like pointerX
or pointerY
) to your app’s logic. In the Rive editor, you assign these properties to elements like positions, scales, or rotations. At runtime, your code accesses and updates them through the viewModelInstance
—allowing for real-time, declarative control without needing a State Machine.
With that in mind, here’s how the new setup replaces the old input-driven logic:
import { useRive } from '@rive-app/react-canvas';
import { useEffect, useState } from 'react';
export function ParallaxEffect({ className }: { className?: string }) {
const [isLoaded, setIsLoaded] = useState(false);
const { rive, setCanvasRef, setContainerRef } = useRive({
src: 'https://cdn.rive.app/animations/hero.riv',
autoplay: true,
autoBind: true,
onLoad: () => setIsLoaded(true),
});
useEffect(() => {
if (!rive) return;
const vmi = rive.viewModelInstance;
const pointerX = vmi?.number('pointerX');
const pointerY = vmi?.number('pointerY');
if (!pointerX || !pointerY) return;
const handleMouseMove = (e: MouseEvent) => {
const { innerWidth, innerHeight } = window;
const x = (e.clientX / innerWidth) * 100;
const y = 100 - (e.clientY / innerHeight) * 100;
pointerX.value = x;
pointerY.value = y;
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
rive.cleanup();
};
}, [rive]);
return (
<div
ref={setContainerRef}
className={`rive-container ${className ?? ''} ${isLoaded ? 'show' : 'hide'}`}
>
<canvas ref={setCanvasRef} />
</div>
);
}
The Result
You get the same parallax effect, but:
- without input variables or blending;
- without a State Machine;
- with simple control via the ViewModel.
Official Live Example from Rive
👉 CodeSandbox: Data Binding Parallax
Conclusion
Data Binding is a major step forward for interactive Rive animations. Effects like parallax can now be set up faster, more reliably, and with cleaner logic. We strongly recommend this approach for new projects.
Final Thoughts
So why did we choose Rive over Lottie for this project?
- Interactivity: With Lottie, achieving the same level of interactivity would’ve required building a custom logic layer from scratch. With Rive, we got that behavior baked into the file—plug and play.
- Optimization: Rive gives you more control over each asset inside the .riv file, and the output tends to be lighter overall.
Our biggest takeaway? Don’t be afraid to experiment with new tools—especially when they feel like the right fit for your project’s concept. Rive matched the playful, interactive vibe of Valley Adventures perfectly, and we’re excited to keep exploring what it can do.