Implementing Rive Animations on a Website
Rive is a tool for creating interactive animations with State Machine. Unlike Lottie (linear playback), Rive animations respond to events: clicks, hover, data input, external triggers. The runtime is lightweight (~40 KB gzip), renders via WebGL or Canvas 2D.
Installation
npm install @rive-app/react-canvas
# or WebGL renderer (better for complex scenes):
npm install @rive-app/react-webgl2
Basic Integration
// components/RiveAnimation.tsx
'use client'
import { useRive, Layout, Fit, Alignment } from '@rive-app/react-canvas'
interface RiveAnimationProps {
src: string
stateMachine?: string
animation?: string
className?: string
}
export function RiveAnimation({
src,
stateMachine,
animation,
className,
}: RiveAnimationProps) {
const { RiveComponent } = useRive({
src,
stateMachines: stateMachine ? [stateMachine] : undefined,
animations: animation ? [animation] : undefined,
autoplay: true,
layout: new Layout({
fit: Fit.Contain,
alignment: Alignment.Center,
}),
})
return <RiveComponent className={className} />
}
State Machine: State Management
State Machine is Rive's main feature. The designer creates a transition graph between animations, the developer manages input parameters:
// components/InteractiveButton.tsx
'use client'
import { useRive, useStateMachineInput } from '@rive-app/react-canvas'
export function RiveButton() {
const { RiveComponent, rive } = useRive({
src: '/animations/button.riv',
stateMachines: 'ButtonSM',
autoplay: true,
})
// Get inputs from State Machine
const isHoverInput = useStateMachineInput(rive, 'ButtonSM', 'isHover')
const isPressedInput = useStateMachineInput(rive, 'ButtonSM', 'isPressed')
const isLoadingInput = useStateMachineInput(rive, 'ButtonSM', 'isLoading')
const handleClick = async () => {
if (isLoadingInput) isLoadingInput.value = true
await fetch('/api/action')
if (isLoadingInput) isLoadingInput.value = false
}
return (
<button
className="relative w-48 h-14"
onMouseEnter={() => isHoverInput && (isHoverInput.value = true)}
onMouseLeave={() => isHoverInput && (isHoverInput.value = false)}
onMouseDown={() => isPressedInput && (isPressedInput.value = true)}
onMouseUp={() => isPressedInput && (isPressedInput.value = false)}
onClick={handleClick}
>
<RiveComponent />
</button>
)
}
Triggers and Numeric Inputs
State Machine supports three input types:
- Boolean — toggle (hover, active, visible)
- Number — numeric value (progress, level, speed)
- Trigger — one-time event (click, success, error)
// components/ProgressRive.tsx
'use client'
import { useRive, useStateMachineInput } from '@rive-app/react-canvas'
interface ProgressRiveProps {
progress: number // 0–100
}
export function ProgressRive({ progress }: ProgressRiveProps) {
const { RiveComponent, rive } = useRive({
src: '/animations/progress.riv',
stateMachines: 'ProgressSM',
autoplay: true,
})
const progressInput = useStateMachineInput(rive, 'ProgressSM', 'progress')
const completeTrigger = useStateMachineInput(
rive,
'ProgressSM',
'complete',
false // this is a trigger, not boolean
)
// Synchronize progress on prop change
if (progressInput) progressInput.value = progress
if (progress >= 100 && completeTrigger) completeTrigger.fire()
return <RiveComponent style={{ width: 300, height: 80 }} />
}
Tracking Events from Rive
Rive can send events back to JavaScript:
// components/RiveWithEvents.tsx
'use client'
import { useEffect } from 'react'
import { useRive, EventType, RiveEvent } from '@rive-app/react-canvas'
export function RiveWithEvents() {
const { RiveComponent, rive } = useRive({
src: '/animations/interactive.riv',
stateMachines: 'Main',
autoplay: true,
})
useEffect(() => {
if (!rive) return
const handler = (event: RiveEvent) => {
const { name, properties } = event.data as any
switch (name) {
case 'ButtonClicked':
console.log('Rive button clicked, data:', properties)
break
case 'AnimationComplete':
console.log('Animation complete')
break
}
}
rive.on(EventType.RiveEvent, handler)
return () => rive.off(EventType.RiveEvent, handler)
}, [rive])
return <RiveComponent style={{ width: 400, height: 300 }} />
}
Optimization: Manual Canvas
For maximum control (multiple Rive instances, custom render loop):
// components/LowLevelRive.tsx
'use client'
import { useEffect, useRef } from 'react'
import Rive, { Fit } from '@rive-app/canvas'
export function LowLevelRive({ src }: { src: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (!canvasRef.current) return
const r = new Rive({
canvas: canvasRef.current,
src,
autoplay: true,
onLoad: () => {
r.resizeDrawingSurfaceToCanvas()
},
})
// Handle resize
const observer = new ResizeObserver(() => {
r.resizeDrawingSurfaceToCanvas()
})
observer.observe(canvasRef.current)
return () => {
r.cleanup()
observer.disconnect()
}
}, [src])
return (
<canvas
ref={canvasRef}
style={{ width: '100%', height: '100%' }}
/>
)
}
Typical Timelines
Playback of ready .riv file — 2–3 hours. Integration with State Machine, inputs, events — 1–2 working days. Creation of .riv file in Rive Editor (if not ready) — separate task for designer/motion designer.







