Rive en Angular: wrapper de la comunidad vs canvas directo
Por el equipo de Charmotion · Actualizado el

Rive no tiene un runtime oficial para Angular. ng-rive figura como desactualizado, y el wrapper de la comunidad que aparece en la documentación de Rive, @grandgular/rive-angular-*, requiere Angular 18+ y tiene pocos usuarios. Para productos de larga duración, llama al runtime web oficial @rive-app/canvas desde un componente: créalo después de que se inicialice la vista, contrólalo con signals y libera los recursos al destruirlo.
Puntos clave
- Rive incluye Angular solo como runtime de la comunidad; no es de su propiedad ni lo mantiene.
- ng-rive figura como desactualizado; los paquetes de Grandgular son más recientes, pero poco adoptados.
- El runtime web directo necesita unas 30 líneas en un componente standalone y no agrega dependencias.
- Protege la creación con
isPlatformBrowser(o usaafterNextRender) para SSR. - Envía los valores del signal a
viewModelInstance.number("mood")coneffect().
¿Existe una librería oficial de Rive para Angular?
No. La documentación de Rive incluye Angular entre los runtimes de la comunidad y aclara que no es un proyecto propiedad de Rive ni mantenido por Rive. Tienes tres opciones realistas:
| Opción | Mantenido por | Angular | Estado |
|---|---|---|---|
@rive-app/canvas o @rive-app/webgl2 usados directamente | Rive (runtime web oficial) | Cualquiera | La más segura a largo plazo |
@grandgular/rive-angular-webgl2 / -canvas | Comunidad, listado en la documentación de Rive | 18+ | Activo, pocos usuarios |
ng-rive | Comunidad | Versiones antiguas | Documentado como desactualizado |
Un wrapper te ahorra algo de código repetitivo, pero agrega una dependencia que tiene que seguir el ritmo de las versiones de Angular y de Rive. Cuando revisamos el repositorio de Grandgular para esta guía, tenía apenas unas pocas estrellas en GitHub. Eso no significa que el código sea malo, pero sí que menos personas detectan y corrigen problemas. El enfoque directo que verás a continuación usa solo el paquete que publica Rive, así que las actualizaciones del runtime nunca dependen de un tercero.
¿ng-rive sigue teniendo mantenimiento?
Considéralo legacy. La documentación antigua de Rive para Angular, que explicaba npm install ng-rive, también se marcó como desactualizada, y la documentación actual de Rive remite en su lugar al wrapper de Grandgular. ng-rive se escribió para versiones antiguas de Angular y es anterior a los componentes standalone y a los signals.
Si una app existente lo usa y funciona, no hace falta quitarlo hoy. Planifica la migración la próxima vez que actualices Angular o adoptes una función más reciente de los archivos de Rive. Data Binding es la principal: Rive dejó obsoletos los inputs de la máquina de estados en favor de los view models, y un wrapper escrito antes de Data Binding no lo expone.
El paquete anterior del propio proyecto Grandgular, @grandgular/rive-angular, también está obsoleto en favor de los paquetes separados -webgl2 y -canvas. Si eliges el wrapper, instala los nombres actuales:
npm install @grandgular/rive-angular-webgl2 @rive-app/webgl2
# or
npm install @grandgular/rive-angular-canvas @rive-app/canvas
Su README indica como mínimo Angular 18.0.0 y TypeScript 5.4. Revisa el README para conocer los nombres actuales del componente y de los inputs antes de escribir las plantillas.
¿Cómo uso @rive-app/canvas directamente en un componente de Angular?
Instala el runtime con npm install @rive-app/canvas, coloca character.riv en public/ (o en src/assets/ en estructuras de proyecto más antiguas) y crea un componente standalone:
import { AfterViewInit, Component, ElementRef, OnDestroy, PLATFORM_ID,
ViewChild, effect, inject, input } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { Rive } from "@rive-app/canvas";
@Component({
selector: "app-mascot",
standalone: true,
template: '<canvas #c width="320" height="320" role="img" aria-label="Animated mascot"></canvas>',
})
export class MascotComponent implements AfterViewInit, OnDestroy {
mood = input(0);
@ViewChild("c") canvas!: ElementRef<HTMLCanvasElement>;
private platformId = inject(PLATFORM_ID);
private r?: Rive;
constructor() {
effect(() => this.setMood(this.mood()));
}
ngAfterViewInit() {
if (!isPlatformBrowser(this.platformId)) return;
this.r = new Rive({
src: "character.riv",
canvas: this.canvas.nativeElement,
stateMachine: "CharacterViewModelMachine",
autoplay: true,
autoBind: true,
onLoad: () => {
this.r?.resizeDrawingSurfaceToCanvas();
this.setMood(this.mood());
},
});
}
fire(name: "success" | "error" | "yes" | "no" | "enter" | "exit") {
this.r?.viewModelInstance?.trigger(name)?.trigger();
}
private setMood(value: number) {
const prop = this.r?.viewModelInstance?.number("mood");
if (prop) prop.value = value;
}
ngOnDestroy() {
this.r?.cleanup();
}
}
cleanup() libera los objetos de C++ que el runtime reservó en WebAssembly, así que llámalo siempre al destruir el componente.
¿Cómo controlo el número mood desde un signal?
El componente anterior ya lo hace: mood es un signal input, y el effect() del constructor se vuelve a ejecutar cada vez que cambia y escribe el valor en el view model vinculado. Como el effect solo lee el signal y escribe en el runtime de Rive, no dispara la detección de cambios ni crea un ciclo.
Es posible que el effect se ejecute antes de que el archivo termine de cargar. El setter comprueba que viewModelInstance exista, y onLoad aplica el valor actual en cuanto el archivo está listo, así que el primer estado de ánimo nunca se pierde.
En un componente padre, deriva el estado de ánimo del estado de la app y llama a fire() para las reacciones puntuales:
import { Component, signal } from "@angular/core";
@Component({
selector: "app-save",
standalone: true,
imports: [MascotComponent],
template: `<app-mascot #m [mood]="saving() ? 5 : 0" />
<button (click)="save(m)">Save</button>`,
})
export class SaveComponent {
saving = signal(false);
async save(m: MascotComponent) {
this.saving.set(true); // mood 5: loading
try { await this.api.save(); m.fire("success"); }
catch { m.fire("error"); }
finally { this.saving.set(false); } // mood 0: idle
}
}

Usa los estados de ánimo para estados continuos (idle, loading, listening) y los triggers para eventos (success, error). Esa división encaja limpiamente con signals para el estado y métodos para los eventos.
¿Cómo evito errores de SSR con el renderizado en servidor de Angular?
El renderizador de servidor de Angular ejecuta tus componentes en Node.js, donde no hay canvas, no hay window ni forma de ejecutar el runtime de Rive. Crear la instancia en el servidor lanza un error o desperdicia trabajo. La comprobación isPlatformBrowser(this.platformId) en ngAfterViewInit sale antes en el servidor, así que ahí solo se renderiza el markup del canvas y la animación empieza después de la hidratación.
Angular también ofrece afterNextRender, que según su documentación se ejecuta solo en el navegador, después del renderizado. Es una alternativa más limpia a la comprobación de plataforma:
constructor() {
afterNextRender(() => this.start()); // never runs on the server
effect(() => this.setMood(this.mood()));
}
En cualquier caso, conserva la limpieza en ngOnDestroy; con optional chaining no hace nada en el servidor. Si el propio import llega a dar problemas en un build de servidor, carga el runtime de forma diferida dentro del callback exclusivo del navegador con await import("@rive-app/canvas").
Los archivos de Charmotion están hechos justo para esta integración: un .riv con 17 animaciones de tu personaje o logo, un view model Character con un número mood de 0 a 9 y seis triggers, además de inputs heredados de CharacterStateMachine para wrappers antiguos. Los sets también incluyen código de ejemplo para web, React y SwiftUI.
Paquete canvas vs WebGL2: ¿cuál elegir?
Ambos paquetes exponen la misma clase Rive y las mismas opciones, así que cambiar de uno a otro solo requiere modificar una línea de import.
@rive-app/canvasdibuja con la API de canvas 2D del navegador. Rive lo recomienda para páginas que muestran muchas animaciones a la vez, porque no está sujeto al límite de contextos WebGL del navegador.@rive-app/webgl2usa el Rive Renderer sobre WebGL2 y admite todas las funciones, incluidos efectos que el canvas 2D no puede dibujar. Rive lo sugiere para la mayoría de las páginas con una sola animación.
Para un panel con una pequeña mascota de marca en varias tarjetas, elige canvas. Para un personaje grande en la sección hero con efectos avanzados, elige WebGL2. El wrapper de Grandgular publica un paquete para cada uno, con la misma elección.
Elijas el que elijas, instala solo uno. Ambos incluyen su propio WASM, y distribuir dos runtimes duplica la descarga sin ningún beneficio. Si más adelante cambias, la ruta del wasm alojado por ti también cambia, así que actualiza al mismo tiempo cualquier import de RuntimeLoader.setWasmUrl().
Cómo agregar un personaje de Rive a una app de Angular sin wrapper
- Instala el runtime. Ejecuta
npm install @rive-app/canvasy agrega el .riv apublic/. - Crea un componente standalone. Agrega un canvas con una referencia de plantilla y un signal input
mood. - Inicia solo en el navegador. Crea
new Rive({ stateMachine: "CharacterViewModelMachine", autoBind: true, ... })enngAfterViewInitdetrás deisPlatformBrowser, o enafterNextRender. - Sincroniza el signal. Usa
effect()para escribir el estado de ánimo enviewModelInstance.number("mood"). - Expón los triggers. Agrega un método
fire(name)que llame atrigger(name).trigger(). - Limpieza. Llama a
this.r?.cleanup()enngOnDestroy.
Preguntas frecuentes
¿Debo usar el wrapper de Rive de Grandgular?
Es el wrapper de la comunidad que la documentación de Rive incluye actualmente, requiere Angular 18 o posterior y está en desarrollo activo, pero todavía pocos proyectos lo usan. Es razonable para prototipos. Para un producto que vas a mantener durante años, el runtime web directo evita depender de un paquete pequeño de terceros.
¿Rive funciona con Angular sin zonas (zoneless)?
Sí. El runtime de Rive maneja su propio ciclo de renderizado con requestAnimationFrame y no depende de Zone.js ni de la detección de cambios. Los signals y effect() envían los valores directamente al runtime, así que el componente anterior funciona igual con o sin zonas. No necesitas envolverlo en NgZone.runOutsideAngular.
¿Dónde debe ir el archivo .riv en un proyecto de Angular?
En los proyectos actuales de Angular, colócalo en public/ y cárgalo como character.riv. Las estructuras más antiguas sirven src/assets/, así que la ruta pasa a ser assets/character.riv. La carpeta que uses debe figurar en la configuración de assets del build, algo que los proyectos nuevos ya traen por defecto.
Respuestas rápidas de esta guía
Fuentes
- Community runtimes: Angular, Rive
- Grandgular/rive: Angular wrapper for Rive, GitHub
- ng-rive, npm
- Web (JS) runtime, Rive
- Web runtime: Data binding, Rive
- Canvas vs WebGL2, Rive
- Server-side and hybrid rendering, Angular
- Signals, Angular