Skip to content

Browser applications ​

The browser package reads a notebook export through HTTP. Build the deterministic quickstart first. Its dist/report directory contains weekly and monthly exported states, an inline summary, and an asset-backed rendered report.

Create the Vite application ​

Install Node.js and pnpm, then create a Vite TypeScript application beside dist/:

bash
pnpm create vite browser --template vanilla-ts
cd browser
pnpm install
pnpm add @marimo-team/marimo-export
mkdir -p public/export
cp -R ../dist/report/. public/export/

The copy makes the complete notebook export available at /export/ through Vite's static-file server. Copy the complete directory so index.json and every declared asset remain together.

Replace index.html with two state buttons and hosts for both outputs:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Quickstart report</title>
  </head>
  <body>
    <main id="app">
      <div aria-label="Report period">
        <button type="button" data-state="weekly">Weekly</button>
        <button type="button" data-state="monthly">Monthly</button>
      </div>
      <p id="status" role="status" aria-live="polite">Loading weekly</p>
      <pre id="summary"></pre>
      <pre id="report"></pre>
    </main>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Replace src/main.ts with the browser reader:

ts
import { openExport } from "@marimo-team/marimo-export";
import { jsonLoader } from "@marimo-team/marimo-export/loader/json";
import { marimoOutputLoader } from "@marimo-team/marimo-export/loader/marimo-output";

const notebookExport = await openExport("/export/");
const summaryHost = required<HTMLPreElement>("#summary");
const reportHost = required<HTMLPreElement>("#report");
const status = required<HTMLParagraphElement>("#status");

let pending: AbortController | undefined;

const show = async (name: "weekly" | "monthly"): Promise<void> => {
  pending?.abort();
  const current = new AbortController();
  pending = current;
  status.textContent = `Loading ${name}`;

  try {
    const state = notebookExport.state(name);
    const options = { signal: current.signal };
    const [summary, report] = await Promise.all([
      state.output("summary").load(jsonLoader(), options),
      state.output("report").load(marimoOutputLoader(), options),
    ]);
    if (report.output?.mimetype !== "text/markdown" || typeof report.output.data !== "string") {
      throw new Error("The report output is not rendered Markdown");
    }
    const parsed = new DOMParser().parseFromString(report.output.data, "text/html");

    current.signal.throwIfAborted();
    summaryHost.textContent = JSON.stringify(summary, null, 2);
    reportHost.textContent = parsed.body.textContent?.trim() ?? "";
    status.textContent = `${name} ready`;
    for (const button of document.querySelectorAll<HTMLButtonElement>("[data-state]")) {
      button.setAttribute("aria-pressed", String(button.dataset.state === name));
    }
  } catch (error) {
    if (!current.signal.aborted) {
      showError(error);
      current.abort();
    }
  } finally {
    if (pending === current) pending = undefined;
  }
};

for (const button of document.querySelectorAll<HTMLButtonElement>("[data-state]")) {
  button.addEventListener("click", () => {
    const name = button.dataset.state === "monthly" ? "monthly" : "weekly";
    void show(name);
  });
}

function required<T extends Element>(selector: string): T {
  const element = document.querySelector<T>(selector);
  if (element === null) throw new Error(`${selector} is missing`);
  return element;
}

function showError(error: unknown): void {
  console.error(error);
  status.textContent = "The notebook export could not be read";
}

window.addEventListener("pagehide", () => pending?.abort(), { once: true });

await show("weekly");

Start the application:

bash
pnpm dev --host 127.0.0.1

Open the printed loopback URL. The page first shows:

text
weekly ready
{
  "days": 7,
  "label": "Last 7 days"
}
Last 7 days
Selected window: 7 days

Selecting Monthly changes both outputs:

text
{
  "days": 30,
  "label": "Last 30 days"
}
Last 30 days
Selected window: 30 days

openExport() validates canonical index.json. The JSON loader reads the inline summary. The rendered-output loader verifies and decodes the selected report asset as an inert snapshot. The example presents its Markdown as text, so it does not attach notebook-authored markup to the page.

state(name) selects an authored alias. resolve(inputs) selects an exact complete input vector. state.resolve(patch) applies a sparse root-input patch to the current vector and selects the matching exported state. Resolution runs no notebook Python.

Keep the latest selection visible ​

show() cancels the pending transition when another button is selected. Both outputs load under the same signal and commit together after the signal check. A failed load leaves the previous outputs visible and reports the error. A superseded load cannot replace the outputs, selected button, or status message.

Some third-party decoders and browser module evaluations can finish after cancellation. Dispose any staged resources they created when that work settles.

Mount an interactive output ​

Some loaders return a value with mount(element). A mount returns an idempotent disposable handle. After building the market dashboard, serve its export at /market-export/. Install the Vega-Lite loader's vega-embed peer runtime:

bash
pnpm add vega-embed

Add a mount host to the page:

html
<div id="chart"></div>

Load and mount the dashboard's performance_chart output:

ts
import { openExport } from "@marimo-team/marimo-export";
import { vegaLiteLoader } from "@marimo-team/marimo-export/loader/vegalite";

const marketExport = await openExport("/market-export/");
const state = marketExport.defaultState;
const host = document.querySelector<HTMLElement>("#chart");
if (host === null) throw new Error("Chart host is missing");

const chart = await state.output("performance_chart").load(vegaLiteLoader({ actions: false }));
const mounted = await chart.mount(host, {
  renderer: "svg",
});

window.addEventListener("pagehide", () => void mounted.dispose(), { once: true });

Keep the mounted view alive while it is visible. Dispose it during route teardown or after a replacement commits. For a complete replacement, mount new values in connected offscreen hosts, confirm that the transition remains current, replace the visible hosts, then dispose the previous mount owner. A failed staged mount leaves the last committed document visible.

The image loader uses the portable filename as alternative text when one is available. The application must provide a meaningful accessible name or nearby text when that filename does not describe the image.

Opening, resolving, verifying, and built-in data loaders operate on inert records. A custom loader executes application-supplied code during load(). Mounting a chart, AnyWidget, or custom interactive value can execute more code with page authority. Review the integrity and trust boundary before deployment.

Follow a changing publication ​

Use the prepared package subpath when a server exposes one mutable manifest route and immutable export instances. Serve a prepared publication develops the producer and browser sides of that handoff.

Use the browser reader reference for exact methods and the loader reference for loader options, result types, peers, cancellation, and disposal.

Released under the Apache 2.0 License.