Best viewed on desktop Mobile Friendly tutorial coming soon!

A Rive Animation

Marko is declarative and reactive, but the wider JavaScript world is full of imperative libraries — canvas renderers, map engines, chart toolkits — that want a raw DOM node and expect you to new them up, call methods on them, and tear them down by hand. Wrapping one in a Marko component is a specific, repeatable pattern; once you’ve done it once, you can wrap almost anything.

Rive is a good example: it plays interactive .riv animations by drawing onto a <canvas>. Its runtime, @rive-app/canvas, is framework-agnostic — hand it a canvas element and a .riv file and it takes over. Our job is to give it a Marko home.

The wrapper’s shape is already in place:

import * as rive from "@rive-app/canvas"
<let/instance=null>
<canvas/canvasEl width=input.width height=input.height/>
<script>
/* start the runtime here */
</script>
<return=instance>

Three pieces do the work:

  • <canvas/canvasEl> renders the canvas and gives you a handle to the real DOM node — canvasEl() returns it.
  • <script> is a client effect: it runs once the tag is mounted in the browser (exactly when that DOM node exists), and it’s where imperative setup belongs.
  • <return=instance> hands the running instance back to whoever used the tag, so a parent could call .play(), .pause(), and so on.

Start the runtime (your job)

Open src/tags/rive-canvas.marko and fill in the <script>. Create a Rive instance pointed at the canvas, remember it, and make sure it’s cleaned up:

<script>
const r = new rive.Rive({
src: input.src,
canvas: canvasEl(),
autoplay: true,
stateMachines: input.stateMachines,
onLoad() { r.resizeDrawingSurfaceToCanvas() },
});
instance = r;
$signal.onabort = () => r.cleanup();
</script>

Save, and the animation comes to life in the preview. Two details carry the whole pattern: canvas: canvasEl() connects the runtime to the DOM node Marko rendered, and $signal.onabort = () => r.cleanup() is the part people forget — when the tag unmounts (or the effect re-runs), Marko fires $signal, and this hands the library its chance to release the canvas, timers, and memory. Skip it and you leak.

That’s the wrapper pattern: hand an imperative runtime a DOM node inside a <script> effect, clean it up on $signal, and return its instance — Marko’s reactive world and an imperative library, cooperating.

Powered by WebContainers Built with TutorialKit Built with ❤️ by defunkt-dev
Files
Preparing Environment
  • Installing dependencies
  • Starting dev server