By following this guide, you will build a complete Rive animation workflow: from designing a state machine in the Rive editor, to integrating it into a live web UI with the official Rive JavaScript runtime. The whole process takes roughly two to three hours the first time through, and about thirty minutes once you know the steps.

What You'll Build

  • A Rive state machine with at least two interactive states (idle and hover) exported as a .riv file
  • A working JavaScript integration that drives the state machine from user events in the browser
  • A reusable canvas component pattern you can copy across projects
  • A performance baseline using the Rive runtime's built-in renderer, targeting 60fps on mid-range devices

Prerequisites

  • A free Rive account (the free tier covers everything in this guide)
  • Node.js 20 or later and a package manager (npm 10 or pnpm 9 work fine)
  • A basic familiarity with HTML, CSS, and JavaScript
  • A project scaffold: a plain Vite app (npm create vite@latest my-rive-ui -- --template vanilla) is enough

Step 1: Create Your Artboard in the Rive Editor

Open Rive and create a new file. Name the artboard something descriptive, like ButtonIcon. Set the artboard dimensions to match how the animation will appear on screen. A 64x64 artboard works well for icon-sized UI elements.

Why does artboard size matter at this stage?

Rive scales the canvas to fit the artboard's aspect ratio at runtime. If you design at the wrong ratio, the runtime will letterbox or crop your animation. Getting this right now avoids a layout fix later.

Draw your shapes using Rive's vector tools. Keep paths simple. Complex path counts above roughly 200 nodes per shape start to show frame drops on mobile at 60fps. Use groups to organise elements you plan to animate together.

Common pitfall: Do not import SVG files with embedded raster images. Rive renders vector paths with its own renderer. Embedded bitmaps bypass that renderer and increase file size significantly.

Step 2: Set Up a State Machine

In the Animate tab, create a new state machine. Name it ButtonInteraction. Add two animation clips first: one named Idle and one named Hover.

What animations should each clip contain?

The Idle clip should be a looping animation at the resting state. A subtle breathing scale or opacity pulse works well. The Hover clip should be a one-shot animation that plays when the user triggers an interaction, then holds on the final frame.

Switch to the State Machine editor. You will see your two clips as state nodes. Connect them with transitions. Add a Boolean input named isHovered. Set the transition from Idle to Hover to trigger when isHovered is true. Set the reverse transition to trigger when isHovered is false.

Pro tip: Set the transition duration on both connections to around 150ms. Faster than that feels abrupt. Slower than 300ms starts to feel laggy on a UI component.

Step 3: Export the .riv File

Click the export button in the top menu. Choose the Web target. Rive exports a binary .riv file. Keep this file small. A typical icon animation should stay under 20KB. If your export exceeds that, check for unused artboards or redundant keyframes.

When should you use the low-level versus high-level export?

For web UI integration, always use the standard export. The low-level export is intended for game engine pipelines. Using it in a browser context requires manual renderer setup that adds complexity without benefit for most SMB projects.

Place the exported ButtonIcon.riv file in your project's public folder (or static folder, depending on your build tool).

Step 4: Install the Rive JavaScript Runtime

In your project terminal, run the following:

npm install @rive-app/canvas

As of September 2026, the @rive-app/canvas package is on version 2.x. This version uses the Rive Renderer, a WebGL2-backed path renderer that outperforms the Canvas 2D fallback by roughly 2x on complex scenes. It falls back to Canvas 2D automatically on devices without WebGL2 support.

Common pitfall: Do not install @rive-app/webgl separately. That package is now deprecated. The canvas package bundles both renderers and selects the right one at runtime.

Step 5: Write the Canvas Component

Create a file called rive-button.js in your src folder. Add the following:

import { Rive, Layout, Fit, Alignment } from '@rive-app/canvas';

const canvas = document.getElementById('rive-canvas');

const r = new Rive({
  src: '/ButtonIcon.riv',
  canvas: canvas,
  stateMachines: 'ButtonInteraction',
  layout: new Layout({
    fit: Fit.Contain,
    alignment: Alignment.Center,
  }),
  autoplay: true,
  onLoad: () => {
    r.resizeDrawingSurfaceToCanvas();
  },
});

canvas.addEventListener('mouseenter', () => {
  const inputs = r.stateMachineInputs('ButtonInteraction');
  const isHovered = inputs.find(i => i.name === 'isHovered');
  if (isHovered) isHovered.value = true;
});

canvas.addEventListener('mouseleave', () => {
  const inputs = r.stateMachineInputs('ButtonInteraction');
  const isHovered = inputs.find(i => i.name === 'isHovered');
  if (isHovered) isHovered.value = false;
});

In your index.html, add a canvas element with a matching ID and your desired display size:

<canvas id="rive-canvas" width="64" height="64"></canvas>

Why call resizeDrawingSurfaceToCanvas inside onLoad?

The Rive runtime needs the canvas to be fully mounted before it can read its pixel dimensions. Calling resizeDrawingSurfaceToCanvas in the onLoad callback ensures the drawing surface matches the canvas's physical size on high-density (Retina) displays. Without this call, animations look blurry on 2x screens.

Step 6: Handle Cleanup and Memory

Rive instances hold references to WebGL contexts. If you mount and unmount components frequently, you must clean up. Add a cleanup function:

function destroyRive() {
  r.cleanup();
}

// Call destroyRive when the component is removed from the DOM
// For example, in a single-page app route change handler

If you are using React or Vue, call r.cleanup() in the component's unmount lifecycle hook. Skipping this step causes WebGL context exhaustion after roughly 16 active canvases in Chrome, which crashes all WebGL rendering on the page.

What if the animation does not play after mounting?

Check that autoplay is set to true and that the state machine name in your code exactly matches the name you gave it in the Rive editor. State machine names are case-sensitive. A mismatch silently fails with no console error in versions before 2.17.

Step 7: Test Performance in the Browser

Open Chrome DevTools and go to the Performance tab. Record a five-second session while hovering the animation repeatedly. Look at the frame rate chart. Target a consistent 60fps with GPU rasterisation enabled.

If frames drop below 50fps, check your animation for large gradient fills or excessive bone counts. Gradients in Rive are rendered per-frame. A complex radial gradient on a 200x200 artboard can cost 2ms per frame on a mid-range laptop GPU.

Also check the number of draw calls in the Performance panel. A well-optimised Rive animation should produce fewer than 10 draw calls per frame.

Pro tip: Use Rive's built-in rive:// debug URL scheme during development. It exposes frame timing data in the browser console. Enable it by appending ?riveDebug=true to your local dev URL.

Step 8: Extend the Pattern to Multiple Animations

Once the single-animation pattern works, wrap it in a factory function so you can reuse it across your UI:

import { Rive, Layout, Fit, Alignment } from '@rive-app/canvas';

export function createRiveComponent({
  canvasId,
  src,
  stateMachine,
}) {
  const canvas = document.getElementById(canvasId);
  const r = new Rive({
    src,
    canvas,
    stateMachines: stateMachine,
    layout: new Layout({ fit: Fit.Contain, alignment: Alignment.Center }),
    autoplay: true,
    onLoad: () => r.resizeDrawingSurfaceToCanvas(),
  });
  return {
    setInput(name, value) {
      const inputs = r.stateMachineInputs(stateMachine);
      const input = inputs.find(i => i.name === name);
      if (input) input.value = value;
    },
    destroy() {
      r.cleanup();
    },
  };
}

This pattern is close to what the team at Lenka Studio uses when building interactive web UIs for clients. A shared factory function means each product team only writes animation-specific logic, not runtime boilerplate.

If your project uses a social media or content layer that needs consistent motion branding across channels, pair this animation system with a documented component library. You can also grab Lenka Studio's free social media toolkit to align your content calendar with your new animated brand assets.

Frequently Asked Questions

Does Rive work with React or Next.js?

Yes. Rive publishes a first-party React package at @rive-app/react-canvas. It wraps the canvas runtime in a hook-based API. For Next.js 14 and later, mark your Rive component with 'use client' because the runtime accesses the DOM directly.

What is the difference between Rive and Lottie?

Lottie is a JSON-based format that replays pre-baked keyframe data. Rive uses a binary format with a built-in state machine, so animations can respond to runtime inputs without re-exporting. Rive files are typically 2 to 5 times smaller than equivalent Lottie JSON exports for interactive animations.

Will Rive animations affect my Core Web Vitals score?

A small, well-optimised Rive animation (under 20KB, below 10 draw calls) has negligible impact on LCP and CLS. Avoid loading the Rive runtime in the critical path. Use a dynamic import or defer loading until the canvas element is in the viewport.

How do I make Rive animations accessible?

Add a descriptive aria-label to the canvas element and set role="img". For animations that convey state changes, also use aria-live on a nearby hidden text element that reflects the current state in words. The Rive runtime itself does not generate accessibility output.

Can I use Rive with Tailwind CSS?

Yes. Size the canvas element with Tailwind utility classes on the HTML element. The Rive layout system reads the canvas's rendered dimensions at load time. Combine Tailwind's responsive prefixes with the onLoad resize call to handle breakpoint changes correctly.

Next Steps

You now have a working Rive animation workflow from artboard to runtime. From here, you can add trigger inputs to your state machine for click events, connect Rive inputs to real data (like a loading percentage), or build a full motion design system where each component has its own .riv file and state machine.

If you want to extend this into a broader design system with documented tokens and component specs, the Lenka Studio team works with product teams across Australia, Singapore, Canada, and the US to build exactly that kind of system. Get in touch to talk through your project.