Rive en Vue 3 y Nuxt: configuración, Data Binding y SSR
Por el equipo de Charmotion · Actualizado el

Rive no tiene un runtime oficial para Vue, así que las apps de Vue 3 y Nuxt usan directamente el runtime web @rive-app/canvas. Crea la instancia de Rive en onMounted con autoBind: true, asigna las propiedades del view model desde un watcher y llama a cleanup() en onUnmounted. En Nuxt, renderiza el componente solo en el cliente, porque canvas y WASM necesitan un navegador.
Puntos clave
- No existe un paquete de Rive para Vue;
@rive-app/canvaso@rive-app/webgl2funcionan en cualquier componente de Vue 3. - Crea la instancia en
onMounted, nunca en el nivel superior desetup, y no la metas enref(). - Con
autoBind: true, asignar.viewModelInstance.number("mood").valuey llama a.trigger("success").trigger(). - Llama a
r.cleanup()enonUnmountedpara liberar los objetos WASM. - En Nuxt, usa un componente
.client.vueo<ClientOnly>para evitar errores en el servidor.
¿Existe un paquete oficial de Rive para Vue?
No. Rive mantiene runtimes oficiales para web (JavaScript), React, plataformas de Apple, Android, Flutter y React Native, entre otros. Su documentación menciona cuatro runtimes de la comunidad (Angular, C#, Qt y RiveCMP para Compose Multiplatform) y ninguno es para Vue. Una antigua página de ayuda de Rive que recomendaba a los desarrolladores de Vue usar el runtime web ya fue eliminada, pero el consejo sigue siendo válido.
No es una carencia tan grande como parece. El runtime web no depende de ningún framework: necesita un elemento <canvas>, un archivo .riv y el nombre de una máquina de estados (state machine). Vue te da justo los hooks que necesitas para proporcionarlos en el momento adecuado.
| Necesidad | Paquete | Notas |
|---|---|---|
| Rive en Vue o Nuxt | @rive-app/canvas | Renderizador de canvas 2D; sin límite de contextos WebGL |
| Rive con efectos avanzados | @rive-app/webgl2 | Misma API, Rive Renderer sobre WebGL2 |
| Lottie en Vue | @lottiefiles/dotlottie-vue | Componente oficial de LottieFiles |
| Renderizado en servidor de Nuxt | <ClientOnly> o .client.vue | Canvas y WASM solo funcionan en el navegador |
Rive describe @rive-app/canvas como la opción adecuada para páginas con muchos canvas animados, porque no está sujeto al límite de contextos WebGL del navegador. Elige @rive-app/webgl2 si tu archivo usa funciones que requieren el Rive Renderer; el código de abajo es idéntico salvo por el import.
¿Cómo cargo un .riv en onMounted y lo limpio en onUnmounted?
Instala el runtime con npm install @rive-app/canvas y coloca el archivo en public/, que tanto Vite como Nuxt sirven desde la raíz del sitio. Luego crea un componente que gestione una instancia de Rive:
<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>
El onMounted de Vue se ejecuta cuando el DOM del componente ya existe, así que el ref del canvas ya está asignado. useTemplateRef requiere Vue 3.5; en versiones anteriores usa const canvas = ref(null). Guarda la instancia de Rive en una variable simple o en shallowRef: un proxy reactivo profundo alrededor de un objeto respaldado por WASM añade sobrecarga y puede romper las comprobaciones de identidad dentro del runtime.
cleanup() importa más de lo que parece. La documentación de Rive indica que elimina los objetos C++ de bajo nivel creados durante la vida de la instancia. Sin él, cada cambio de ruta que vuelve a montar la mascota provoca una fuga de memoria.
¿Cómo cambio de estado de ánimo y disparo triggers desde Vue?
El componente anterior ya hace ambas cosas. El watch sobre props.mood escribe el nuevo valor en el view model vinculado, y el método expuesto fire() reproduce una reacción puntual. Un componente padre lo controla como a cualquier otro componente:
<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>

Mantén el estado de la app como fuente de verdad y deriva de él el estado de ánimo, como hace el binding :mood. Los triggers son eventos, no estado, así que pasan por un método en lugar de una prop.
Si un archivo usa el sistema de inputs antiguo en lugar de view models, el mismo patrón funciona con r.stateMachineInputs("CharacterStateMachine"): busca el input por nombre, asigna .value a un número y llama a .fire() en un trigger. La guía de la máquina de estados explica ambos sistemas.
¿Cómo uso Rive en Nuxt sin errores de SSR?
Nuxt renderiza las páginas primero en el servidor, donde no hay window, ni canvas, ni cargador de WebAssembly para Rive. onMounted nunca se ejecuta en el servidor, así que el componente anterior ya es bastante seguro. El riesgo que queda es el propio import: si un módulo accede a globales del navegador al importarse, el bundle del servidor lanza window is not defined.
Nuxt te ofrece dos formas de excluir el componente del renderizado en servidor:
- Nombra el archivo
CharacterRive.client.vue. Así Nuxt lo renderiza solo en el cliente y queda fuera del renderizado en servidor. - Envuélvelo en
<ClientOnly>. El slot se renderiza solo en el navegador, y el slot#fallbackse renderiza en el servidor.
<ClientOnly>
<CharacterRive :mood="2" />
<template #fallback>
<img src="/character-poster.png" width="320" height="320" alt="">
</template>
</ClientOnly>
La imagen de respaldo mantiene estable el diseño y ofrece algo que ver a los rastreadores y a las conexiones lentas. Si prefieres mantener un único componente universal, mueve el import dentro del hook: const { Rive } = await import("@rive-app/canvas") dentro de un onMounted asíncrono. Registra onUnmounted de forma síncrona en setup, no después del await, o Vue no podrá asociarlo al componente.
¿Cómo alojo rive.wasm en mi propio servidor con Vite o Nuxt?
Por defecto, el runtime web descarga rive.wasm desde unpkg la primera vez que creas una instancia. Es cómodo, pero añade una solicitud a un tercero y falla en entornos sin conexión o restringidos. La documentación de Rive explica cómo apuntar el runtime a tu propia copia con RuntimeLoader.setWasmUrl() y subraya que la versión del archivo wasm debe coincidir con la versión instalada del paquete @rive-app.
Con Vite (que es lo que usa Nuxt), el sufijo ?url copia el archivo en tu build y devuelve su URL con hash, así que la versión siempre coincide con 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);
Pon esto en un pequeño módulo que importe el componente de Rive, o en un plugin de cliente de Nuxt (plugins/rive.client.js). Como la URL proviene del paquete instalado, al actualizar @rive-app/canvas se actualiza automáticamente el wasm. No copies rive.wasm a mano en public/: con la siguiente actualización del paquete quedarían desincronizados.
Alojarlo tú mismo también te permite precargar el archivo en páginas donde la mascota aparece sin hacer scroll, con un <link rel="preload"> para la URL del wasm, de modo que se descargue en paralelo con tu JavaScript.
¿Rive o Lottie para una mascota de marca en Vue?
Ambos funcionan en Vue. La elección depende de si tu código necesita indicarle a la animación qué hacer a continuación.
- Lottie es adecuado para clips lineales: una marca de éxito, una ilustración de estado vacío, un spinner. LottieFiles publica
@lottiefiles/dotlottie-vue, que renderiza<DotLottieVue autoplay loop src="…" />y reproduce archivos .lottie y .json. Cambiar de estado de ánimo implica cambiar de archivo o de segmento. - Rive es adecuado para personajes con estados. Un solo .riv contiene todos los loops y reacciones, la máquina de estados gestiona las transiciones y tu componente solo escribe un número o dispara un trigger.
Si todavía no tienes un archivo de Rive, Charmotion crea uno a partir de la imagen de un personaje o logo en unos minutos: 17 animaciones en un solo .riv con un view model Character (un número mood de 0 a 9 para idle, happy, wave, thinking, sad, loading, listening, talking, sleeping y attention, además de los triggers success, error, yes, no, enter y exit). El archivo también conserva los inputs heredados de CharacterStateMachine, y el set incluye exportaciones en MP4, GIF, y WebM y MOV con fondo transparente para los lugares donde un canvas no encaja.

Cómo añadir un personaje de Rive a una app de Vue 3 o Nuxt
- Instala el runtime web. Ejecuta
npm install @rive-app/canvasy coloca el archivo .riv enpublic/. - Crea el componente. Añade un canvas con un template ref y crea
new Rive({ ..., stateMachine: "CharacterViewModelMachine", autoBind: true })enonMounted. - Vincula el estado de ánimo. Observa la prop
moody asignar.viewModelInstance.number("mood").value, también una vez enonLoad. - Expón los triggers. Expón un método que llame a
r.viewModelInstance.trigger(name).trigger()para success y error. - Limpieza. Llama a
r.cleanup()enonUnmounted. - Gestiona el SSR de Nuxt. Nombra el archivo
.client.vueo envuélvelo en<ClientOnly>con una imagen póster como alternativa.
Preguntas frecuentes
¿Puedo usar @rive-app/react-canvas en una app de Vue?
No. El paquete de React depende de los hooks y del renderizado de React. En Vue, usa @rive-app/canvas o @rive-app/webgl2, los runtimes web independientes del framework que el propio paquete de React envuelve. La API para cargar archivos, vincular view models y hacer la limpieza es la misma que se documenta en las páginas del runtime web de Rive.
¿Por qué Nuxt dice que window is not defined cuando importo Rive?
La importación se ejecutó durante el renderizado en el servidor, donde no existen las variables globales del navegador. Cambia el nombre del componente a CharacterRive.client.vue para que Nuxt lo renderice solo en el navegador, envuélvelo en <ClientOnly> o importa @rive-app/canvas de forma dinámica dentro de onMounted. Cualquiera de las tres opciones mantiene el runtime fuera del servidor.
¿La instancia de Rive debe ir en un ref?
Guárdala en una variable simple o en un shallowRef. Un ref() profundo envuelve la instancia en un proxy reactivo, lo que añade sobrecarga y puede interferir con los objetos respaldados por WASM que contiene. Tu template nunca necesita leer la instancia directamente, así que no hace falta que sea reactiva.
¿El watcher del estado de ánimo se ejecuta antes de que cargue el archivo?
Puede ocurrir, y por eso el setter comprueba que viewModelInstance exista antes de escribir. Después, el callback onLoad aplica el valor actual de la prop cuando el archivo está listo, así que el primer estado de ánimo nunca se pierde. A partir de ahí, cada cambio de prop va directo al view model vinculado.
Respuestas rápidas de esta guía
Fuentes
- Web (JS) runtime, Rive
- Web runtime: Data binding, Rive
- Preloading WASM, Rive
- Canvas vs WebGL2, Rive
- Lifecycle hooks, Vue.js
- Template refs, Vue.js
- <ClientOnly> component, Nuxt
- dotlottie-web: players for web, React, Vue, Svelte and web components, LottieFiles on GitHub