Interactive Artifacts

Static docs can't show a sorting algorithm. Interactive artifacts can.

Create a code artifact with a .app.js extension. When you open it, you'll see a Run button. Click it, and the app comes alive — a visualization you can manipulate, a prototype you can click through, a diagram that updates as you watch.

The Basics

Export an object with a render function. That's the minimum.

// hello-world.app.js
export default {
  render(container, ctx) {
    container.innerHTML = '<h1 style="color: #fff;">Hello, Miriad!</h1>';
  }
}

The function receives:

  • container — A DOM element to render into
  • ctx — Runtime context with dimensions, animation helpers, and storage

Animation

For anything that moves, use ctx.loop(). It calls your function every frame with the time delta.

export default {
  render(container, ctx) {
    const canvas = document.createElement('canvas');
    canvas.width = ctx.width;
    canvas.height = ctx.height;
    container.appendChild(canvas);
    
    const c = canvas.getContext('2d');
    let x = 0;

    this.stop = ctx.loop((dt) => {
      x = (x + dt * 0.1) % ctx.width;
      
      c.fillStyle = '#111';
      c.fillRect(0, 0, ctx.width, ctx.height);
      c.fillStyle = '#0ff';
      c.beginPath();
      c.arc(x, ctx.height / 2, 20, 0, Math.PI * 2);
      c.fill();
    });
  },

  cleanup() {
    this.stop?.();
  }
}

Always implement cleanup() if you use ctx.loop(). It's called when the user stops the app or navigates away. Without it, you'll leak memory.

Adding Controls

Mix HTML controls with canvas graphics.

export default {
  render(container, ctx) {
    container.innerHTML = `
      <div style="margin-bottom: 1rem; color: #fff;">
        <label>Speed: <input type="range" id="speed" min="1" max="10" value="5"></label>
        <button id="reset">Reset</button>
      </div>
      <canvas width="${ctx.width}" height="${ctx.height - 50}"></canvas>
    `;

    const canvas = container.querySelector('canvas');
    const speedSlider = container.querySelector('#speed');
    const resetBtn = container.querySelector('#reset');
    
    let x = 0;
    resetBtn.onclick = () => { x = 0; };

    this.stop = ctx.loop((dt) => {
      const speed = parseFloat(speedSlider.value);
      x = (x + speed * dt * 0.05) % canvas.width;
      
      const c = canvas.getContext('2d');
      c.fillStyle = '#111';
      c.fillRect(0, 0, canvas.width, canvas.height);
      c.fillStyle = '#f80';
      c.fillRect(x, canvas.height / 2 - 10, 20, 20);
    });
  },

  cleanup() {
    this.stop?.();
  }
}

Persisting State

Use ctx.store to save data that survives when the user stops and restarts the app.

render(container, ctx) {
  let highScore = ctx.store.get('highScore') || 0;
  
  // ... game logic ...
  
  if (score > highScore) {
    highScore = score;
    ctx.store.set('highScore', highScore);
  }
}

Storage is scoped to the artifact. Each .app.js has its own isolated store.

Loading External Libraries

Import any npm package from esm.sh. Make your render function async.

export default {
  async render(container, ctx) {
    container.innerHTML = '<div style="color:#fff;">Loading...</div>';
    
    const THREE = await import('https://esm.sh/three@0.160.0');
    
    const scene = new THREE.Scene();
    // ...
  }
}

Common libraries:

LibraryImport
Three.jshttps://esm.sh/three@0.160.0
D3https://esm.sh/d3@7
Chart.jshttps://esm.sh/chart.js@4/auto
GSAPhttps://esm.sh/gsap@3
Matter.jshttps://esm.sh/matter-js@0.19
Tone.jshttps://esm.sh/tone@14

Pin your versions. three@0.160.0, not just three. Avoids breaking changes.

Fetching Data from Other Artifacts

Your app can load data from other artifacts on the board.

export default {
  async render(container, ctx) {
    const response = await fetch('/channels/my-channel/assets/data.json');
    const data = await response.json();
    
    // Visualize it
  }
}

This works for any artifact type — JSON, markdown, images, whatever you've put on the board.

Tips

Use ctx dimensions. Don't hardcode sizes. ctx.width and ctx.height update automatically when the container resizes.

Keep cleanup simple. Store your stop function on this, call it in cleanup. That's usually all you need.

Show loading states. If you're importing heavy libraries, tell the user something's happening.

Start simple. Get a basic version working, then add complexity. Easier to debug.

When to Use

  • Explaining algorithms — Step through sorting, pathfinding, tree traversal
  • Data visualization — Charts and graphs that respond to filters
  • Prototypes — Clickable mockups before building the real thing
  • Simulations — Physics, particles, game mechanics
  • Tools — Calculators, converters, generators

When text isn't enough, build something.