Rive Interactive - State Machine-Based Vector Animation
Overview
Rive is a state machine-based animation platform that enables designers to create interactive vector animations with complex logic and runtime interactivity. Unlike timeline-only animation tools (like Lottie), Rive supports state machines, input handling, and two-way data binding between application code and animations.
Key Features:
- State machine system for complex interactive logic
- ViewModel API for two-way data binding
- Input handling (boolean, number, trigger inputs)
- Custom events for animation-to-code communication
- Runtime property control (colors, strings, numbers, enums)
- Cross-platform support (Web, React, React Native, iOS, Android, Flutter)
- Small file sizes with vector graphics
When to Use This Skill:
- Creating UI animations with complex state transitions
- Building interactive animated components (buttons, toggles, loaders)
- Implementing game-like UI with state-driven animations
- Binding real-time data to animated visualizations
- Creating animations that respond to user input
- Working with designer-created animations requiring runtime control
Alternatives:
- Lottie (lottie-animations): For simpler timeline-based animations without state machines
- Framer Motion (motion-framer): For code-first React animations with spring physics
- GSAP (gsap-scrolltrigger): For timeline-based web animations with precise control
Core Concepts
1. State Machines
State machines define animation behavior with states and transitions:
- States: Different animation states (e.g., idle, hover, pressed)
- Inputs: Variables that control transitions (boolean, number, trigger)
- Transitions: Rules for moving between states
- Listeners: React hooks to respond to state changes
Three input types control state machine behavior:
- Boolean: On/off states (e.g., isHovered, isActive)
- Number: Numeric values (e.g., progress, volume)
- Trigger: One-time events (e.g., click, submit)
3. ViewModels
Data binding system for dynamic properties:
- String Properties: Text content (e.g., username, title)
- Number Properties: Numeric data (e.g., stock price, score)
- Color Properties: Dynamic colors (hex values)
- Enum Properties: Selection from predefined options
- Trigger Properties: Animation events
4. Events
Custom events emitted from animations:
- General Events: Custom named events
- Event Properties: Data attached to events
- Event Listeners: React hooks to handle events
Common Patterns
Pattern 1: Basic Rive Animation
Use Case: Display a simple Rive animation in React
Implementation:
bash
1# Installation
2npm install rive-react
jsx
1import Rive from 'rive-react';
2
3export default function SimpleAnimation() {
4 return (
5 <Rive
6 src="animation.riv"
7 artboard="Main"
8 animations="idle"
9 layout={{ fit: "contain", alignment: "center" }}
10 style={{ width: '400px', height: '400px' }}
11 />
12 );
13}
Key Points:
src: Path to .riv file
artboard: Which artboard to display
animations: Which animation timeline to play
layout: How animation fits in container
Use Case: Control animation states based on user interaction
Implementation:
jsx
1import { useRive, useStateMachineInput } from 'rive-react';
2
3export default function InteractiveButton() {
4 const { rive, RiveComponent } = useRive({
5 src: 'button.riv',
6 stateMachines: 'Button State Machine',
7 autoplay: true,
8 });
9
10 // Get state machine inputs
11 const hoverInput = useStateMachineInput(
12 rive,
13 'Button State Machine',
14 'isHovered',
15 false
16 );
17
18 const clickInput = useStateMachineInput(
19 rive,
20 'Button State Machine',
21 'isClicked',
22 false
23 );
24
25 return (
26 <div
27 onMouseEnter={() => hoverInput && (hoverInput.value = true)}
28 onMouseLeave={() => hoverInput && (hoverInput.value = false)}
29 onClick={() => clickInput && clickInput.fire()} // Trigger input
30 style={{ cursor: 'pointer' }}
31 >
32 <RiveComponent style={{ width: '200px', height: '100px' }} />
33 </div>
34 );
35}
Input Types:
- Boolean:
input.value = true/false
- Number:
input.value = 50
- Trigger:
input.fire()
Pattern 3: ViewModel Data Binding
Use Case: Bind application data to animation properties
Implementation:
jsx
1import { useRive, useViewModel, useViewModelInstance,
2 useViewModelInstanceString, useViewModelInstanceNumber } from 'rive-react';
3import { useEffect, useState } from 'react';
4
5export default function Dashboard() {
6 const [stockPrice, setStockPrice] = useState(150.0);
7
8 const { rive, RiveComponent } = useRive({
9 src: 'dashboard.riv',
10 autoplay: true,
11 autoBind: false, // Manual binding for ViewModels
12 });
13
14 // Get ViewModel and instance
15 const viewModel = useViewModel(rive, { name: 'Dashboard' });
16 const viewModelInstance = useViewModelInstance(viewModel, { rive });
17
18 // Bind properties
19 const { setValue: setTitle } = useViewModelInstanceString(
20 'title',
21 viewModelInstance
22 );
23
24 const { setValue: setPrice } = useViewModelInstanceNumber(
25 'stockPrice',
26 viewModelInstance
27 );
28
29 useEffect(() => {
30 if (setTitle) setTitle('Stock Dashboard');
31 }, [setTitle]);
32
33 useEffect(() => {
34 if (setPrice) setPrice(stockPrice);
35 }, [setPrice, stockPrice]);
36
37 // Simulate real-time updates
38 useEffect(() => {
39 const interval = setInterval(() => {
40 setStockPrice((prev) => prev + (Math.random() - 0.5) * 10);
41 }, 1000);
42
43 return () => clearInterval(interval);
44 }, []);
45
46 return <RiveComponent style={{ width: '800px', height: '600px' }} />;
47}
ViewModel Property Hooks:
useViewModelInstanceString - Text properties
useViewModelInstanceNumber - Numeric properties
useViewModelInstanceColor - Color properties (hex)
useViewModelInstanceEnum - Enum selection
useViewModelInstanceTrigger - Animation triggers
Pattern 4: Handling Rive Events
Use Case: React to events emitted from Rive animation
Implementation:
jsx
1import { useRive, EventType, RiveEventType } from 'rive-react';
2import { useEffect } from 'react';
3
4export default function InteractiveRating() {
5 const { rive, RiveComponent } = useRive({
6 src: 'rating.riv',
7 stateMachines: 'State Machine 1',
8 autoplay: true,
9 automaticallyHandleEvents: true,
10 });
11
12 useEffect(() => {
13 if (!rive) return;
14
15 const onRiveEvent = (event) => {
16 const eventData = event.data;
17
18 if (eventData.type === RiveEventType.General) {
19 console.log('Event:', eventData.name);
20
21 // Access event properties
22 const rating = eventData.properties.rating;
23 const message = eventData.properties.message;
24
25 if (rating >= 4) {
26 alert(`Thanks for ${rating} stars: ${message}`);
27 }
28 }
29 };
30
31 rive.on(EventType.RiveEvent, onRiveEvent);
32
33 return () => {
34 rive.off(EventType.RiveEvent, onRiveEvent);
35 };
36 }, [rive]);
37
38 return <RiveComponent style={{ width: '400px', height: '300px' }} />;
39}
Pattern 5: Preloading Rive Files
Use Case: Optimize load times by preloading animations
Implementation:
jsx
1import { useRiveFile, useRive } from 'rive-react';
2
3export default function PreloadedAnimation() {
4 const { riveFile, status } = useRiveFile({
5 src: 'large-animation.riv',
6 });
7
8 const { RiveComponent } = useRive({
9 riveFile: riveFile,
10 artboard: 'Main',
11 autoplay: true,
12 });
13
14 if (status === 'loading') {
15 return <div>Loading animation...</div>;
16 }
17
18 if (status === 'failed') {
19 return <div>Failed to load animation</div>;
20 }
21
22 return <RiveComponent style={{ width: '600px', height: '400px' }} />;
23}
Pattern 6: Controlled Animation with Refs
Use Case: Control animation from parent component
Implementation:
jsx
1import { useRive, useViewModel, useViewModelInstance,
2 useViewModelInstanceTrigger } from 'rive-react';
3import { useImperativeHandle, forwardRef } from 'react';
4
5const AnimatedComponent = forwardRef((props, ref) => {
6 const { rive, RiveComponent } = useRive({
7 src: 'logo.riv',
8 autoplay: true,
9 autoBind: false,
10 });
11
12 const viewModel = useViewModel(rive, { useDefault: true });
13 const viewModelInstance = useViewModelInstance(viewModel, { rive });
14
15 const { trigger: spinTrigger } = useViewModelInstanceTrigger(
16 'triggerSpin',
17 viewModelInstance
18 );
19
20 // Expose methods to parent
21 useImperativeHandle(ref, () => ({
22 spin: () => spinTrigger && spinTrigger(),
23 pause: () => rive && rive.pause(),
24 play: () => rive && rive.play(),
25 }));
26
27 return <RiveComponent style={{ width: '200px', height: '200px' }} />;
28});
29
30export default function App() {
31 const animationRef = useRef();
32
33 return (
34 <div>
35 <AnimatedComponent ref={animationRef} />
36 <button onClick={() => animationRef.current?.spin()}>Spin</button>
37 <button onClick={() => animationRef.current?.pause()}>Pause</button>
38 </div>
39 );
40}
Pattern 7: Multi-Property ViewModel Updates
Use Case: Update multiple animation properties from complex data
Implementation:
jsx
1import { useRive, useViewModel, useViewModelInstance,
2 useViewModelInstanceString, useViewModelInstanceNumber,
3 useViewModelInstanceColor } from 'rive-react';
4import { useEffect } from 'react';
5
6export default function UserProfile({ user }) {
7 const { rive, RiveComponent } = useRive({
8 src: 'profile.riv',
9 autoplay: true,
10 autoBind: false,
11 });
12
13 const viewModel = useViewModel(rive, { useDefault: true });
14 const viewModelInstance = useViewModelInstance(viewModel, { rive });
15
16 // Bind all properties
17 const { setValue: setName } = useViewModelInstanceString('name', viewModelInstance);
18 const { setValue: setScore } = useViewModelInstanceNumber('score', viewModelInstance);
19 const { setValue: setColor } = useViewModelInstanceColor('avatarColor', viewModelInstance);
20
21 useEffect(() => {
22 if (user && setName && setScore && setColor) {
23 setName(user.name);
24 setScore(user.score);
25 setColor(parseInt(user.color.substring(1), 16)); // Convert hex to number
26 }
27 }, [user, setName, setScore, setColor]);
28
29 return <RiveComponent style={{ width: '300px', height: '300px' }} />;
30}
Integration Patterns
With Framer Motion (motion-framer)
Animate container while Rive handles interactive content:
jsx
1import { motion } from 'framer-motion';
2import Rive from 'rive-react';
3
4export default function AnimatedCard() {
5 return (
6 <motion.div
7 initial={{ opacity: 0, y: 20 }}
8 animate={{ opacity: 1, y: 0 }}
9 whileHover={{ scale: 1.05 }}
10 >
11 <Rive
12 src="card.riv"
13 stateMachines="Card State Machine"
14 style={{ width: '300px', height: '400px' }}
15 />
16 </motion.div>
17 );
18}
Trigger Rive animations on scroll:
jsx
1import { useRive, useStateMachineInput } from 'rive-react';
2import { useEffect, useRef } from 'react';
3import gsap from 'gsap';
4import ScrollTrigger from 'gsap/ScrollTrigger';
5
6gsap.registerPlugin(ScrollTrigger);
7
8export default function ScrollRive() {
9 const containerRef = useRef();
10 const { rive, RiveComponent } = useRive({
11 src: 'scroll-animation.riv',
12 stateMachines: 'State Machine 1',
13 autoplay: true,
14 });
15
16 const trigger = useStateMachineInput(rive, 'State Machine 1', 'trigger');
17
18 useEffect(() => {
19 if (!trigger) return;
20
21 ScrollTrigger.create({
22 trigger: containerRef.current,
23 start: 'top center',
24 onEnter: () => trigger.fire(),
25 });
26 }, [trigger]);
27
28 return (
29 <div ref={containerRef}>
30 <RiveComponent style={{ width: '100%', height: '600px' }} />
31 </div>
32 );
33}
1. Use Off-Screen Renderer
jsx
1<Rive
2 src="animation.riv"
3 useOffscreenRenderer={true} // Better performance
4/>
2. Optimize Rive Files
In Rive Editor:
- Keep artboards under 2MB
- Use vector graphics (avoid raster images when possible)
- Minimize number of bones in skeletal animations
- Reduce complexity of state machines
3. Preload Critical Animations
jsx
1const { riveFile } = useRiveFile({ src: 'critical.riv' });
2// Preload during app initialization
4. Disable Automatic Event Handling
jsx
1<Rive
2 src="animation.riv"
3 automaticallyHandleEvents={false} // Manual control
4/>
Common Pitfalls and Solutions
Problem: useStateMachineInput returns null
Solution:
jsx
1// ❌ Wrong: Incorrect input name
2const input = useStateMachineInput(rive, 'State Machine', 'wrongName');
3
4// ✅ Correct: Match exact name from Rive editor
5const input = useStateMachineInput(rive, 'State Machine', 'isHovered');
6
7// Always check if input exists before using
8if (input) {
9 input.value = true;
10}
Pitfall 2: ViewModel Property Not Updating
Problem: ViewModel property doesn't update animation
Solution:
jsx
1// ❌ Wrong: autoBind enabled
2const { rive } = useRive({
3 src: 'dashboard.riv',
4 autoplay: true,
5 // autoBind: true (default)
6});
7
8// ✅ Correct: Disable autoBind for ViewModels
9const { rive } = useRive({
10 src: 'dashboard.riv',
11 autoplay: true,
12 autoBind: false, // Required for manual ViewModel control
13});
Pitfall 3: Event Listener Not Firing
Problem: Rive events not triggering callback
Solution:
jsx
1// ❌ Wrong: Missing automaticallyHandleEvents
2const { rive } = useRive({
3 src: 'rating.riv',
4 stateMachines: 'State Machine 1',
5 autoplay: true,
6});
7
8// ✅ Correct: Enable event handling
9const { rive } = useRive({
10 src: 'rating.riv',
11 stateMachines: 'State Machine 1',
12 autoplay: true,
13 automaticallyHandleEvents: true, // Required for events
14});
Resources
Official Documentation
Rive Editor
Learning Resources
- lottie-animations: For simpler timeline-based animations without state machines
- motion-framer: For code-first React animations with gestures
- gsap-scrolltrigger: For scroll-driven animations
- spline-interactive: For 3D interactive animations
Scripts
This skill includes utility scripts:
component_generator.py - Generate Rive React component boilerplate
viewmodel_builder.py - Build ViewModel property bindings
Run scripts from the skill directory:
bash
1./scripts/component_generator.py
2./scripts/viewmodel_builder.py
Assets
Starter templates and examples:
starter_rive/ - Complete React + Rive template
examples/ - Real-world integration patterns