Web graphics

A web graphic is a small HTML, CSS and JavaScript page that sits on a slide as a layer: a chart that grows, a number that counts up, a list that reveals line by line. It steps through named states, and cues in your script step it while you talk.

What a web graphic is

A graphic is a folder of files: an entry page, its scripts and styles, any images or fonts it needs, and a graphic.json that names it and lists its steps. When you add it, ArcLight embeds the whole folder in your show document, like an image. Moving or deleting the folder afterward doesn't affect the show.

The page's background is transparent, so the slide shows through. ArcLight draws the page offscreen and treats it like any other layer: it appears on the canvas, in program output, in the presenter view, in recordings and in the virtual camera.

Add a graphic to a slide

  1. Select the slide, then choose Graphics ▸ Web Graphic from Folder… in the toolbar.

  2. Pick the graphic's folder (the one with index.html and graphic.json) and click Open.

  3. The graphic lands centered on the slide at the size in its graphic.json, named after its name. Move and resize it like any layer. Its settings are in Format ▸ Graphic.

If the folder can't be used, ArcLight tells you why: no files, a graphic.json that isn't valid, an entry page that's missing, or a folder over 5 MB. Hidden files in the folder are skipped.

To change a graphic's files, edit the folder and add it again. An AI agent can also replace a graphic's files in place.

The bundle

Everything the page loads must be inside the folder. Link files with relative paths, such as app.js or fonts/Inter.woff2. Libraries have to be copied in too: pages can't load anything from the internet.

graphic.json

Every field is optional, but a graphic without steps can't be stepped from the script.

FieldWhat it does
nameThe layer's name when you add it. Script cues can refer to the graphic by this name.
entryThe page to open. Defaults to index.html.
size[width, height] in canvas points: the layer's size when you add it. Without it, the layer takes half the canvas in each direction.
stepsThe graphic's states, in order, such as ["2023", "2024", "2025"].
paramsDefault values handed to the page, such as {"currency": "USD"}. You can override them per layer in the inspector.
{
  "name": "Revenue chart",
  "entry": "index.html",
  "size": [1080, 600],
  "steps": ["2023", "2024", "2025"],
  "params": { "currency": "USD" }
}

The page is laid out at the layer's size. Resize the layer and the page's viewport resizes with it, so design with relative units or read the window size.

The window.arclight runtime

ArcLight adds a small object, window.arclight, to every graphic before its own scripts run.

APIWhat it does
arclight.paramsThe params from graphic.json, with the layer's own parameters on top.
arclight.stepsThe step names, in order.
arclight.step, arclight.stepIndexThe current step and its position in steps.
arclight.onStep((name, index, {animate}) => …)Called with the step to show. It's also called once when the page loads, with animate false. Draw that state, and animate into it only when animate is true.
arclight.onEnter(fn), arclight.onExit(fn)Called when the graphic's page starts showing and after it stops being shown.
arclight.time()Seconds on ArcLight's clock for this page.
arclight.ready()Optional. Tells ArcLight the first frame is ready to capture.
arclight.mode"program" while it runs on a slide, "render" when ArcLight draws a single still of it.

Steps are absolute. onStep tells you which state to show, not "go forward one". While you rehearse you can jump back or skip ahead, so render the state from the step you're given and never assume what came before it.

Animate by time, never by counting frames

ArcLight runs each graphic on its own clock. performance.now(), Date.now() and requestAnimationFrame all follow that clock, and CSS animations, CSS transitions and Web Animations are paused and moved to the right moment on every frame. That's what keeps a graphic smooth in a page no one is looking at, and makes every frame repeatable: the same step at the same moment always looks the same.

So:

What a graphic can't do

Errors, including exceptions thrown inside your onStep handler, are caught and listed under Page errors in the inspector.

A complete example

This graphic shows a label, counts a number up, then reveals a note: three steps. Make a folder with these two files and add it with Graphics ▸ Web Graphic from Folder….

graphic.json

{
  "name": "Big Number",
  "size": [800, 360],
  "steps": ["intro", "value", "note"],
  "params": {
    "label": "Monthly listeners",
    "value": 48000,
    "note": "Up 12% on last year"
  }
}

index.html

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
  html, body { margin: 0; height: 100%; background: transparent; }
  body { display: grid; place-content: center; text-align: center;
         font-family: -apple-system, sans-serif; color: white; }
  #label { font-size: 34px; opacity: 0.8; }
  #number { font-size: 150px; font-weight: 700; line-height: 1; opacity: 0; }
  #note { font-size: 30px; color: #edb341; opacity: 0; }
</style>
</head>
<body>
  <div id="label"></div>
  <div id="number">0</div>
  <div id="note"></div>
<script>
  const p = arclight.params;
  const label = document.getElementById("label");
  const number = document.getElementById("number");
  const note = document.getElementById("note");
  label.textContent = p.label;
  note.textContent = p.note;

  // Counting runs on ArcLight's clock, so every frame is exact.
  let countStart = null;
  function drawCount() {
    if (countStart === null) return;
    const t = Math.min((arclight.time() - countStart) / 1.2, 1);
    const eased = 1 - Math.pow(1 - t, 3);
    number.textContent = Math.round(p.value * eased).toLocaleString();
    if (t < 1) requestAnimationFrame(drawCount);
  }

  function fade(el, visible, animate) {
    el.getAnimations().forEach(a => a.cancel());
    el.style.opacity = visible ? 1 : 0;
    if (animate) {
      el.animate([{ opacity: visible ? 0 : 1, transform: "translateY(12px)" },
                  { opacity: visible ? 1 : 0, transform: "none" }],
                 { duration: 500, easing: "ease-out" });
    }
  }

  // Steps are absolute: draw the state for this step, whatever came before.
  arclight.onStep((name, index, { animate }) => {
    const showsNumber = index >= 1;
    fade(number, showsNumber, animate && name === "value");
    fade(note, index >= 2, animate && name === "note");
    if (showsNumber && animate && name === "value") {
      countStart = arclight.time();
      requestAnimationFrame(drawCount);
    } else {
      countStart = null;
      number.textContent = showsNumber ? p.value.toLocaleString() : "0";
    }
  });

  arclight.ready();
</script>
</body>
</html>

To show a different number on another slide, select the layer and change value under Parameters in the inspector. The file stays the same.

Step it from the script

In the script, a trigger cue in double braces steps a graphic when the prompter reaches it. Triggers never change slides.

WriteWhat happens
{{Big Number: value}}The graphic named Big Number goes to its value step.
{{value}}The graphic that has a step called value goes to it. If the slide has only one graphic, that one.
{{next}}Runs the live slide's next build, as if you'd pressed the next key. It doesn't step a graphic, and does nothing unless the show is playing.

Names and steps ignore case and extra spaces. In the script editor a trigger becomes a purple chip, such as ◆ Big Number: value or ◆ Next build.

Which slide a trigger belongs to

ArcLight looks for the graphic on the slide named by the last [[Slide]] cue before the trigger. So write the slide cue first, then the triggers for that slide's graphics:

[[Audience]] Here's where we started this year. {{value}} Forty-eight thousand people a month. {{note}} And that's up twelve percent.

Rehearse steps in the inspector

Select the graphic and open Format ▸ Graphic:

While the show isn't playing, triggers in the script step the graphic on the selected slide, so you can read through the prompter and watch each step land. When you press Play, every graphic goes back to its starting step.

One page per graphic. Layers that show the same graphic with the same parameters share one running page, so a graphic keeps its current step across Magic Move and on duplicated slides. To carry a graphic onto the next slide, duplicate the slide or copy the layer rather than adding the folder again: a graphic added twice runs as two separate pages.

Let an agent write it

AI agents connected over MCP can write a graphic's files, add it to a slide, render each step to check it, and put the trigger cues into the script. The producer skill ships a kit of ready-made data graphics. See Set up AI agents.