Ir al contenido
charmotion

Rive en Svelte y SvelteKit (guía de Svelte 5)

Por el equipo de Charmotion · Actualizado el

Ilustración de portada de la guía de frameworks y no-code: Rive en Svelte y SvelteKit (guía de Svelte 5), con personajes de demostración de Charmotion

Svelte no tiene un runtime oficial de Rive, así que usas directamente el runtime web @rive-app/canvas. Crea la instancia dentro de onMount, que nunca se ejecuta durante el renderizado en el servidor, con autoBind: true. Actualiza el número mood del view model desde un $effect, dispara triggers desde los manejadores de eventos y devuelve una función de limpieza síncrona que llame a r.cleanup().

Puntos clave

  • Rive no ofrece un runtime para Svelte; @rive-app/canvas funciona en cualquier componente de Svelte 5.
  • onMount no se ejecuta en el servidor, así que puedes dejar activado el SSR de SvelteKit.
  • Mantén síncrono el callback de onMount para que se ejecute la función de limpieza que devuelve.
  • Guarda la instancia en una variable $state.raw para que $effect se vuelva a ejecutar cuando exista.
  • Coloca los archivos .riv en static/ y cárgalos desde la ruta raíz.

¿Rive tiene un paquete para Svelte?

No hay uno oficial. La documentación de Rive cubre runtimes oficiales para web, React, Apple, Android, Flutter, React Native y motores de juego, y menciona cuatro runtimes de la comunidad: Angular, C#, Qt y RiveCMP. No hay ninguno para Svelte ni SvelteKit. Existen algunos wrappers de la comunidad en npm, pero son proyectos pequeños que pueden quedarse atrás respecto al runtime.

La opción fiable es el propio runtime web de Rive, que es independiente del framework. Un componente de Svelte ya te da todo lo que necesita: un elemento canvas mediante bind:this, un hook de montaje y un hook de desmontaje.

NecesidadPaquete
Personaje interactivo con estados@rive-app/canvas (o @rive-app/webgl2)
Clip lineal de Lottie@lottiefiles/dotlottie-svelte

Instálalo con npm install @rive-app/canvas. Rive recomienda el paquete canvas cuando una página muestra muchas animaciones a la vez, ya que no está sujeto al límite de contextos WebGL del navegador. El paquete WebGL2 tiene la misma API si necesitas el Rive Renderer.

¿Cómo monto Rive en onMount con el SSR activado?

La documentación de Svelte indica que onMount no se ejecuta en un componente renderizado en el servidor, así que crear ahí la instancia de Rive es seguro para SSR sin desactivarlo. Este es un componente completo de Svelte 5:

<script>
  import { onMount } from "svelte";
  import { Rive } from "@rive-app/canvas";

  let { mood = 0 } = $props();
  let canvas;
  let r = $state.raw(null);

  onMount(() => {
    const instance = new Rive({
      src: "/character.riv",
      canvas,
      stateMachine: "CharacterViewModelMachine",
      autoplay: true,
      autoBind: true,
      onLoad: () => {
        instance.resizeDrawingSurfaceToCanvas();
        r = instance; // ready: the $effect below runs now
      },
    });
    return () => instance.cleanup(); // sync callback, so this runs
  });

  $effect(() => {
    const prop = r?.viewModelInstance?.number("mood");
    if (prop) prop.value = mood;
  });

  export function fire(name) {
    r?.viewModelInstance?.trigger(name)?.trigger();
  }
</script>

<canvas bind:this={canvas} width="320" height="320"
        role="img" aria-label="Animated mascot"></canvas>

La importación estática funciona bien en SvelteKit porque nada se ejecuta hasta onMount. Si un bundler o un paquete sigue dando errores durante el renderizado en el servidor, impórtalo de forma dinámica dentro del hook, o protege el código fuera de los componentes con browser de $app/environment.

¿Cómo vinculo una prop mood al view model con $effect?

$effect se vuelve a ejecutar cada vez que cambia un valor reactivo que haya leído. En el componente anterior lee dos: la prop mood y r. Por eso r se declara con $state.raw y se asigna en onLoad: el efecto se ejecuta primero con r todavía en null, no hace nada, y vuelve a ejecutarse en cuanto el archivo está listo, aplicando el estado de ánimo inicial. A partir de ahí, cada cambio de prop se escribe directamente en el view model.

Usar $state.raw en lugar de $state es importante. Un $state normal hace que los objetos sean profundamente reactivos mediante proxies, algo que no quieres alrededor de un objeto del runtime respaldado por WASM. La variante raw solo detecta reasignaciones.

Así, un componente padre controla la mascota de marca con props normales y una referencia al componente:

<script>
  import Mascot from "$lib/Mascot.svelte";
  let mascot;
  let saving = $state(false);

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

<Mascot bind:this={mascot} mood={saving ? 5 : 0} />
<button onclick={save}>Save</button>
Diagrama de una máquina de estados de Rive: un estado de entrada, diez estados de loop elegidos por un número mood y estados de reacción puntual activados por triggers que vuelven al loop actual
Cómo está conectado un .riv de Charmotion: el número mood elige un loop y los triggers reproducen reacciones puntuales desde cualquier estado.

Los estados de ánimo son estados, así que van en props. Los triggers son eventos puntuales, así que pasan por la función exportada fire().

¿Dónde coloco los archivos .riv en SvelteKit?

Colócalos en static/. SvelteKit sirve esa carpeta desde la raíz del sitio, así que static/character.riv se carga como /character.riv, igual que en el componente anterior. Los archivos de esa carpeta se copian tal cual, sin hash, así que pon un nombre nuevo a un archivo modificado (por ejemplo character-v2.riv) o configura headers de caché cortos si lo reemplazas a menudo.

Un archivo .riv suele ser pequeño. La descarga más pesada es el rive.wasm del runtime, que el runtime web obtiene de unpkg por defecto. Para servirlo tú mismo, indica al runtime dónde está antes de crear la primera instancia:

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);

La importación ?url de Vite copia el wasm en tu build y devuelve su URL. La documentación de Rive exige que la versión del wasm coincida con la del paquete @rive-app instalado, y al importarlo desde el paquete eso queda garantizado. Alojarlo tú mismo vale la pena en apps que funcionan sin conexión, con políticas de seguridad de contenido estrictas y en páginas que precargan la mascota.

¿Cómo hago la limpieza al destruir el componente?

Devuelve desde onMount una función que llame a cleanup() en la instancia, como se muestra arriba. La documentación de Rive explica que cleanup() elimina los objetos C++ de bajo nivel creados durante la vida de la instancia; si lo omites, tendrás fugas de memoria en cada navegación que vuelva a montar el componente.

Hay una trampa. Svelte solo usa la función devuelta cuando el callback pasado a onMount es síncrono. Un callback async devuelve una promesa, así que tu limpieza se ignora sin aviso. Si necesitas hacer await de una importación dinámica, usa onDestroy para la limpieza o mantén síncrono el callback externo:

onMount(() => {
  let instance;
  import("@rive-app/canvas").then(({ Rive }) => {
    instance = new Rive({ /* same options as above */ });
  });
  return () => instance?.cleanup();
});

Detén también el trabajo que iniciaste fuera de Rive: elimina los event listeners y desconecta cualquier IntersectionObserver que hayas usado para iniciar la animación de forma diferida.

¿Cómo respeto prefers-reduced-motion?

Comprueba la media query antes de reproducir y mantén quieto el personaje cuando esté activada. prefers-reduced-motion indica la configuración del sistema operativo del visitante y tiene dos valores, no-preference y reduce.

const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

const instance = new Rive({
  // ...same options as above
  autoplay: !reduce,
});

Con autoplay: false el runtime sigue dibujando el primer fotograma de la máquina de estados, así que la mascota aparece en una pose estática. Aun así puedes establecer el estado de ánimo para que la pose se ajuste a la situación. Llama a esto dentro de onMount, porque matchMedia no existe en el servidor.

Para un archivo listo, Charmotion convierte una imagen de personaje o logo en 17 animaciones en pocos minutos, entregadas como un solo .riv con un view model Character: un número mood (0 idle, 1 happy, 2 wave, 3 thinking, 4 sad, 5 loading, 6 listening, 7 talking, 8 sleeping, 9 attention) y los triggers success, error, yes, no, enter y exit, junto con exportaciones bajo demanda de MP4, GIF y WebM y MOV transparentes.

Rive vs dotLottie en Svelte: ¿cuál debería usar?

Usa dotLottie para un clip que se reproduce igual cada vez, y Rive para un personaje que reacciona a tu app.

  • dotLottie: npm install @lottiefiles/dotlottie-svelte y luego renderiza el componente DotLottieSvelte con una fuente .lottie o .json. Ideal para marcas de éxito, ilustraciones de onboarding y animaciones de carga.
  • Rive: un solo .riv contiene idle, loading, success y todos los demás estados, con las transiciones gestionadas por la máquina de estados. Tu código de Svelte solo escribe un número o dispara un trigger.

La pregunta decisiva es si tu código debe indicar qué pasa después. Si la respuesta es "seguir en loop hasta que termine la solicitud y luego celebrar", una máquina de estados (state machine) te ahorra hacer malabares con varios archivos y coordinar tú mismo el paso de uno a otro. Consulta Rive vs Lottie para ver la comparación completa.

Cómo agregar un personaje de Rive a una app de SvelteKit

  1. Instala el runtime. Ejecuta npm install @rive-app/canvas y copia el .riv en static/.
  2. Crea el componente. Vincula un canvas con bind:this y crea la instancia de Rive en un onMount síncrono con autoBind: true.
  3. Controla cuándo está listo. Asigna la instancia a una variable $state.raw en onLoad.
  4. Vincula el estado de ánimo. En $effect, asigna r.viewModelInstance.number("mood").value a partir de la prop.
  5. Dispara triggers. Exporta una función que llame a trigger(name).trigger() en el view model.
  6. Limpieza. Devuelve () => instance.cleanup() desde onMount.

Preguntas frecuentes

¿Necesito desactivar SSR para usar Rive en SvelteKit?

No. La documentación de Svelte indica que onMount no se ejecuta durante el renderizado en el servidor, así que crear ahí la instancia de Rive mantiene la página renderizada en el servidor. Desde el servidor solo se envía el markup del canvas, y la animación empieza cuando el componente se monta en el navegador. No hace falta definir ssr = false para la ruta.

¿Por qué no se ejecuta la limpieza de mi onMount?

Lo más probable es que tu callback de onMount sea async. Svelte solo usa la función de limpieza devuelta cuando el callback es síncrono, porque una función async devuelve una promesa. Mantén el callback síncrono y gestiona el import dinámico con .then(), o mueve la limpieza a onDestroy.

¿Funciona en Svelte 4?

Las llamadas al runtime son las mismas. Reemplaza $props() por export let mood = 0, reemplaza $effect por una sentencia reactiva como $: if (r) setMood(mood) y convierte r en un let normal. onMount, bind:this y la función de limpieza funcionan exactamente igual.

Respuestas rápidas de esta guía

Fuentes

Guías relacionadas