Skip to content
charmotion

Rive in Vue 3 and Nuxt: Setup, Data Binding, SSR

By the Charmotion team · Updated

Cover illustration for the frameworks & no-code guide: Rive in Vue 3 and Nuxt: Setup, Data Binding, SSR, with Charmotion demo characters

Rive has no official Vue runtime, so Vue 3 and Nuxt apps use the web runtime @rive-app/canvas directly. Create the Rive instance in onMounted with autoBind: true, set view model properties from a watcher, and call cleanup() in onUnmounted. In Nuxt, render the component client-only because canvas and WASM need a browser.

Key takeaways

  • There is no Rive package for Vue; @rive-app/canvas or @rive-app/webgl2 works in any Vue 3 component.
  • Create the instance in onMounted, never in setup top level, and keep it out of ref().
  • With autoBind: true, set r.viewModelInstance.number("mood").value and call .trigger("success").trigger().
  • Call r.cleanup() in onUnmounted to free the WASM objects.
  • In Nuxt, use a .client.vue component or <ClientOnly> to avoid server-side errors.

Is there an official Rive package for Vue?

No. Rive maintains official runtimes for the web (JavaScript), React, Apple platforms, Android, Flutter and React Native, among others. Its documentation lists four community runtimes (Angular, C#, Qt and RiveCMP for Compose Multiplatform) and none of them is for Vue. An older Rive help page that told Vue developers to use the web runtime has since been removed, and the advice still holds.

That is less of a gap than it sounds. The web runtime is framework-agnostic: it needs a <canvas> element, a .riv file and a state machine name. Vue gives you exactly the hooks you need to provide those at the right time.

NeedPackageNotes
Rive in Vue or Nuxt@rive-app/canvas2D canvas renderer; no WebGL context limit
Rive with advanced effects@rive-app/webgl2Same API, Rive Renderer on WebGL2
Lottie in Vue@lottiefiles/dotlottie-vueOfficial LottieFiles component
Nuxt server rendering<ClientOnly> or .client.vueCanvas and WASM are browser-only

Rive describes @rive-app/canvas as suited to pages with many animated canvases because it isn't bound by the browser's WebGL context limit. Pick @rive-app/webgl2 if your file uses features that need the Rive Renderer; the code below is identical apart from the import.

How do I load a .riv in onMounted and clean it up in onUnmounted?

Install the runtime with npm install @rive-app/canvas and put the file in public/, which Vite and Nuxt both serve from the site root. Then create a component that owns one Rive instance:

<script setup>
import { watch, onMounted, onUnmounted, useTemplateRef } from "vue";
import { Rive, Layout, Fit } from "@rive-app/canvas";

const props = defineProps({ mood: { type: Number, default: 0 } });
const canvas = useTemplateRef("canvas");
let r = null; // plain variable: don't wrap the instance in ref()

function setMood(value) {
  const prop = r?.viewModelInstance?.number("mood");
  if (prop) prop.value = value;
}

function fire(name) {
  r?.viewModelInstance?.trigger(name)?.trigger();
}

onMounted(() => {
  r = new Rive({
    src: "/character.riv",
    canvas: canvas.value,
    stateMachine: "CharacterViewModelMachine",
    autoplay: true,
    autoBind: true,
    layout: new Layout({ fit: Fit.Contain }),
    onLoad: () => {
      r.resizeDrawingSurfaceToCanvas();
      setMood(props.mood);
    },
  });
});

watch(() => props.mood, setMood);
onUnmounted(() => r?.cleanup());
defineExpose({ fire });
</script>

<template>
  <canvas ref="canvas" width="320" height="320"
          role="img" aria-label="Animated mascot" />
</template>

Vue's onMounted runs after the component's DOM exists, so the canvas ref is set. useTemplateRef needs Vue 3.5; on older versions use const canvas = ref(null). Keep the Rive instance in a plain variable or shallowRef: a deep reactive proxy around a WASM-backed object adds overhead and can break identity checks inside the runtime.

cleanup() matters more than it looks. Rive's docs say it removes the low-level C++ objects created during the instance's life. Without it, every route change that remounts the mascot leaks memory.

How do I switch moods and fire triggers from Vue?

The component above already does both. The watch on props.mood writes the new value to the bound view model, and the exposed fire() method plays a one-shot reaction. A parent drives it like any other component:

<script setup>
import { ref, useTemplateRef } from "vue";
import CharacterRive from "./CharacterRive.vue";

const mascot = useTemplateRef("mascot");
const saving = ref(false);

async function save() {
  saving.value = true;               // mood 5: loading
  try {
    await api.save();
    mascot.value.fire("success");
  } catch {
    mascot.value.fire("error");
  } finally {
    saving.value = false;            // back to mood 0: idle
  }
}
</script>

<template>
  <CharacterRive ref="mascot" :mood="saving ? 5 : 0" />
  <button @click="save">Save</button>
</template>
Diagram of a Rive state machine: an entry state, ten loop states chosen by a mood number, and one-shot trigger states that return to the current loop
How a Charmotion .riv is wired: the mood number picks a loop, triggers play one-shots from any state.

Keep app state as the source of truth and derive the mood from it, as the :mood binding does. Triggers are events, not state, so they go through a method rather than a prop.

If a file uses the older input system instead of view models, the same pattern works with r.stateMachineInputs("CharacterStateMachine"): find the input by name, set .value on a number and call .fire() on a trigger. The state machine guide explains both systems.

How do I use Rive in Nuxt without SSR errors?

Nuxt renders pages on the server first, where there is no window, no canvas and no WebAssembly loader for Rive. onMounted never runs on the server, so the component above is mostly safe already. The remaining risk is the import itself: if a module touches browser globals when it is imported, the server bundle throws window is not defined.

Nuxt gives you two ways to keep the component out of server rendering:

  • Name the file CharacterRive.client.vue. Nuxt then renders it only on the client, and it is left out of the server render.
  • Wrap it in <ClientOnly>. The slot renders only in the browser, and the #fallback slot is rendered on the server.
<ClientOnly>
  <CharacterRive :mood="2" />
  <template #fallback>
    <img src="/character-poster.png" width="320" height="320" alt="">
  </template>
</ClientOnly>

The fallback image keeps the layout stable and gives crawlers and slow connections something to see. If you'd rather keep a single universal component, move the import into the hook: const { Rive } = await import("@rive-app/canvas") inside an async onMounted. Register onUnmounted synchronously in setup, not after the await, or Vue can't associate it with the component.

How do I self-host rive.wasm in Vite or Nuxt?

By default the web runtime downloads rive.wasm from unpkg the first time you create an instance. That is convenient, but it adds a third-party request and fails in offline or locked-down environments. Rive's docs show how to point the runtime at your own copy with RuntimeLoader.setWasmUrl(), and they stress that the wasm file version must match the installed @rive-app package version.

With Vite (which Nuxt uses), the ?url suffix copies the file into your build and returns its hashed URL, so the version always matches package.json:

import { RuntimeLoader } from "@rive-app/canvas";
import riveWasmUrl from "@rive-app/canvas/rive.wasm?url";

// Run once, before the first new Rive(...)
RuntimeLoader.setWasmUrl(riveWasmUrl);

Put this in a small module that the Rive component imports, or in a Nuxt client plugin (plugins/rive.client.js). Because the URL comes from the installed package, upgrading @rive-app/canvas upgrades the wasm automatically. Don't copy rive.wasm into public/ by hand: the next package update would leave the two out of sync.

Self-hosting also lets you preload the file on pages where the mascot sits above the fold, with a <link rel="preload"> for the wasm URL, so it downloads in parallel with your JavaScript.

Rive or Lottie for a Vue mascot?

Both work in Vue. The choice depends on whether your code needs to tell the animation what to do next.

  • Lottie suits linear clips: a success check, an empty-state illustration, a spinner. LottieFiles publishes @lottiefiles/dotlottie-vue, which renders <DotLottieVue autoplay loop src="…" /> and plays .lottie and .json files. Switching between moods means swapping files or segments.
  • Rive suits characters with states. One .riv holds every loop and reaction, the state machine handles transitions, and your component only writes a number or fires a trigger.

If you don't have a Rive file yet, Charmotion builds one from a character or logo image in about one to two hours: 17 animations in a single .riv with a Character view model (a mood number from 0 to 9 for idle, happy, wave, thinking, sad, loading, listening, talking, sleeping and attention, plus success, error, yes, no, enter and exit triggers). The file also keeps the legacy CharacterStateMachine inputs, and the set includes MP4, GIF and transparent WebM and MOV exports for places where a canvas doesn't fit.

Ten looping states of one character, each selected by a mood number from 0 to 9
The ten looping states in one Charmotion Rive file, selected with the mood number input.

How to add a Rive character to a Vue 3 or Nuxt app

  1. Install the web runtime. Run npm install @rive-app/canvas and place the .riv file in public/.
  2. Create the component. Add a canvas with a template ref and create new Rive({ ..., stateMachine: "CharacterViewModelMachine", autoBind: true }) in onMounted.
  3. Bind the mood. Watch the mood prop and set r.viewModelInstance.number("mood").value, also once in onLoad.
  4. Expose triggers. Expose a method that calls r.viewModelInstance.trigger(name).trigger() for success and error.
  5. Clean up. Call r.cleanup() in onUnmounted.
  6. Handle Nuxt SSR. Name the file .client.vue or wrap it in <ClientOnly> with a poster image fallback.

Frequently asked questions

Can I use @rive-app/react-canvas in a Vue app?

No. The React package depends on React hooks and rendering. In Vue, use @rive-app/canvas or @rive-app/webgl2, the framework-agnostic web runtimes that the React package itself wraps. The API for loading files, binding view models and cleaning up is the same one documented on Rive's web runtime pages.

Why does Nuxt say window is not defined when I import Rive?

The import ran during server rendering, where browser globals don't exist. Rename the component to CharacterRive.client.vue so Nuxt renders it only in the browser, wrap it in <ClientOnly>, or import @rive-app/canvas dynamically inside onMounted. Any of the three keeps the runtime off the server.

Should the Rive instance go in a ref?

Keep it in a plain variable or a shallowRef. A deep ref() wraps the instance in a reactive proxy, which adds overhead and can interfere with the WASM-backed objects inside it. Your template never needs to read the instance directly, so it doesn't need to be reactive at all.

Does the mood watcher run before the file loads?

It can, which is why the setter checks that viewModelInstance exists before writing. The onLoad callback then applies the current prop value once the file is ready, so the first mood is never lost. After that, every prop change goes straight to the bound view model.

Quick answers from this guide

Sources

Related guides