Anywidgets¶
Marimo's anywidget integration lets you author
fully interactive UI components as small ES modules. On a live marimo
notebook these render inside a marimo kernel that mediates Python ↔ JS
state; on a static marimo-book page, there's no kernel. marimo-book
makes anywidgets render anyway via a small runtime shim that's loaded
on every page.
Live demo: drawdata¶
drawdata is a small
anywidget — a self-contained ES module
that draws onto a <canvas>. Click and drag in the panel
below to scribble points; press a number key (1–4) before
drawing to switch the active class.
This page is static — there is no Python kernel running.
The widget renders because marimo-book extracts the inlined
ES module from marimo export's output and mounts it via a
~150-line shim (marimo_book.js).
What you can and can't do statically¶
- ✅ The widget renders — anywidget's
_esmtrait is inlined as adata:URL, so no kernel and no CDN are required. - ✅ All client-side interaction works — drawing, brush colours, the canvas itself.
- ❌ The drawn
datacannot flow into a downstream Python cell. That needs a kernel.
For genuine Python reactivity (drawn points → live DataFrame →
live plot) flip the chapter to mode: wasm in book.yml. See
the WASM demo for what that looks like.
How it works¶
- During build,
marimo export ipynboutputs each anywidget as a<marimo-anywidget>custom element with the widget's ES module inlined as a base64 data URL on thedata-js-urlattribute. - The preprocessor rewraps it as
<div class="marimo-book-anywidget" data-js-url="...">. - At page load, a ~150-line JS shim
(
marimo_book.js, bundled viaextra_javascript) finds each mount, dynamically imports the module viaimport(), builds a minimal anywidget-compatiblemodelobject, and callsmodule.default.render({model, el}).
No marimo runtime needed. No WebSocket. Just one JS file and the widget's own ES module.
Seeding widget state¶
Anywidget JS modules typically read initial state via model.get("key").
Since there's no live kernel to provide that state, marimo-book has
to seed it before render() is called.
Two precedence layers — both optional; whichever values exist at each layer are merged (later wins):
1. widget_defaults in book.yml¶
widget_defaults:
CompassWidget:
b0: 3.0
PrecessionWidget:
b0: 3.0
flip_angle: 90.0
t1: 0.0
t2: 0.0
show_relaxation: false
paused: false
One entry per widget class name. Recommended when multiple cells instantiate the same widget and you want them all to share the same defaults.
2. Literal kwargs in the cell¶
@app.cell
def _(mo):
mo.ui.anywidget(PrecessionWidget(flip_angle=30.0, show_relaxation=True))
return
marimo-book walks the cell's AST, finds the widget constructor call
(any CamelCase class ending in Widget, View, or Mount), and
extracts literal kwargs (int, float, bool, str, None, list,
dict). These override widget_defaults for that specific mount.
Troubleshooting¶
"Nothing renders, but I see a placeholder div in DevTools."
- Check the browser console for an import error. A typo in the widget's data URL would show as a syntax error.
- Confirm that
javascripts/marimo_book.jsloaded (Network tab).
"The widget renders but throws Cannot read properties of undefined
in an animation loop."
- The widget's JS is reading a
model.get("key")that isn't seeded. Either add that key towidget_defaultsinbook.yml, or make the widget JS defensive withmodel.get("key") ?? defaultValue.
"I want the widget to persist state between page navigations."
- State lives in the mount
<div>; Material's instant navigation re-runs the shim on every page load. For state that needs to persist, store it inlocalStoragefrom inside your widget's JS.
Plotly figures¶
Plotly figures (anything that produces a plotly.graph_objects.Figure,
including make_subplots, px.scatter, etc.) render fully interactive
on static pages — zoom, pan, hover, the whole toolbar.
The pipeline:
- Marimo's exporter emits each figure as
<marimo-plotly data-figure='{json}' data-config='{json}'>with the complete figure spec inlined. - The preprocessor rewraps it as
<div class="marimo-book-plotly" data-figure='{json}'>. - On page load,
marimo_book.jslazy-loads Plotly.js from jsdelivr (cached after first chapter that has a chart) and callsPlotly.newPlot(mount, data, layout, config)per mount.
No kernel, no extra setup. Just write the figure as you would in any marimo notebook and it renders as the static last expression of its cell.
@app.cell(hide_code=True)
def _(go, x, y):
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=y, mode='lines'))
fig.update_layout(title="My chart", height=400)
fig
return
CSS reserves a 320 px slot before hydration so the page doesn't jump
when Plotly mounts. If your chart needs more space, set
fig.update_layout(height=...) — the Plotly height wins.
Elements we strip¶
Marimo's <marimo-ui-element> wrappers around standalone controls —
<marimo-slider>, <marimo-switch>, <marimo-dropdown>,
<marimo-radio>, <marimo-number>, <marimo-button> — require a
running kernel to be meaningful. marimo-book strips them at
preprocess time. For static pages, use an anywidget that includes its
controls inside the widget itself, opt the page into
mode: wasm, or rely on
precompute.enabled for
discrete-value sliders.
Example: dartbrains widgets¶
Dartbrains ships ten Canvas 2D
and Three.js anywidgets (compass, magnetization, precession, spin
ensemble, k-space, encoding, convolution, transform cube, cost function,
smoothing) that render live on static pages with this pipeline. Its
book.yml widget_defaults block is a good template to copy.