wigglystuff is Vincent Warmerdam’s collection of playful anywidget controls. This notebook is a compatibility probe, not a showcase — it answers two questions:
Do plain wigglystuff widgets survive our static export (MyST +
myst-anywidget-static-export, no kernel)? Do they need a wrapper?Can a wigglystuff widget be linked to a manywidgets widget with no kernel?
Everything below is deliberately dead simple. If a cell renders and reacts on the published site, that mechanism works.
import wigglystuff, manywidgets, anywidget
print("wigglystuff ", wigglystuff.__version__)
print("manywidgets ", manywidgets.__version__)
print("anywidget ", anywidget.__version__)wigglystuff 0.5.26
manywidgets 0.2.0a1
anywidget 0.9.21
1. Does a bare wigglystuff widget render statically?¶
The static-export plugin rewrites any anywidget output whose _esm was captured
into the notebook’s metadata.widgets at execution time. wigglystuff widgets are
ordinary anywidgets that load their JS from _esm = Path(...) — anywidget inlines
that file’s contents into the widget state, so there is nothing manywidgets-specific
about them. Prediction: they just work.
One widget per cell, so a failure names itself.
from wigglystuff import Slider2D
xy = Slider2D(x=0.3, y=-0.2, width=320, height=320)
xyxy.x, xy.y # read the trait back in a live kernel(0.3, -0.2)Knob and CircularSlider are SVG-based and ship a separate _css file — a
second thing the plugin has to carry (it injects _css into the widget’s shadow
root).
from wigglystuff import Knob
knob = Knob(value=42, min_value=0, max_value=100, label="Knob", size=120)
knobfrom wigglystuff import CircularSlider
dial = CircularSlider(value=30, start=0, stop=100, size=200, label="Dial")
dialTangleSlider is the “drag the number in the sentence” control from Bret Victor’s
Tangle — it renders inline in HTML, so it also
tests that the plugin’s block-level widget wrapper doesn’t break inline layout.
from wigglystuff import TangleSlider
tangle = TangleSlider(amount=25, min_value=0, max_value=100, step=1, suffix="%")
tangleMatrix is the most stateful of the simple ones: a list-of-lists trait plus a
CSS file, edited by dragging cells.
from wigglystuff import Matrix
mat = Matrix(rows=3, cols=3, min_value=-5, max_value=5, step=0.5)
mat2. Can wigglystuff and manywidgets widgets be linked?¶
Our links survive static export because the plugin lifts every ipywidgets
LinkModel / DirectionalLinkModel in the notebook’s widget state into a
page-level registry, then re-binds it in the browser against whichever widget
models registered themselves. That machinery keys off model_id — not off
anything manywidgets-specific — so a wigglystuff endpoint should be no different
from a manywidgets one.
Three directions to prove, all with jsdlink.
2a. wigglystuff → manywidgets¶
Drag the knob; the manywidgets Stat and NumberDisplay should follow.
from ipywidgets import jsdlink
from manywidgets import Stat, NumberDisplay, Row, Column
power = Knob(value=60, min_value=0, max_value=100, label="Power", size=110)
readout = NumberDisplay(label="Knob value", format="{:.0f}", duration=200)
stat = Stat(label="Power level", value=60, unit="%")
jsdlink((power, "value"), (readout, "value"))
jsdlink((power, "value"), (stat, "value"))
Row(Column(readout, stat, gap="12px"), gap="24px", align="center")power # rendered separately: Row/Column is a manywidgets container (see 2d)2b. manywidgets → wigglystuff¶
The reverse direction: a manywidgets Slider drives a wigglystuff CircularSlider
and a Knob.
from manywidgets import Slider
driver = Slider(label="Driver", min=0, max=100, step=1, value=35)
followed_dial = CircularSlider(value=35, start=0, stop=100, size=180, label="follows")
followed_knob = Knob(value=35, min_value=0, max_value=100, label="follows", size=110)
jsdlink((driver, "value"), (followed_dial, "value"))
jsdlink((driver, "value"), (followed_knob, "value"))
driverfollowed_dialfollowed_knob2c. wigglystuff → wigglystuff¶
No manywidgets in the loop at all — just the plugin’s link registry between two third-party anywidgets.
pct = TangleSlider(amount=40, min_value=0, max_value=100, step=1, suffix="%")
mirror = Knob(value=40, min_value=0, max_value=100, label="mirrors", size=110)
jsdlink((pct, "amount"), (mirror, "value"))
pctmirror2d. Can a manywidgets container host a wigglystuff child?¶
Row / Column are manywidgets layout widgets that mount their children through
the plugin’s host.renderChild hook. Their children trait is a plain
widget_serialization list, so in principle any anywidget can be a child — this
cell tests whether that holds for a foreign widget.
mixed_knob = Knob(value=70, min_value=0, max_value=100, label="Mix", size=110)
mixed_stat = Stat(label="Mixed layout", value=70, unit="%")
jsdlink((mixed_knob, "value"), (mixed_stat, "value"))
Row(mixed_knob, mixed_stat, gap="24px", align="center")2e. Two-way linking¶
jslink (bidirectional) between a manywidgets Slider and a wigglystuff Knob:
moving either should move the other.
from ipywidgets import jslink
two_way_slider = Slider(label="Two-way", min=0, max=100, step=1, value=50)
two_way_knob = Knob(value=50, min_value=0, max_value=100, label="Two-way", size=110)
jslink((two_way_slider, "value"), (two_way_knob, "value"))
two_way_slidertwo_way_knobWhat we learned¶
Verified against the built static site (just build, served over HTTP, no kernel),
by dragging the real controls in a browser:
1. wigglystuff widgets need no wrapper. Every widget above renders, and every
one accepts input — the knobs and dials drag, the 2D slider tracks the pointer
across its canvas, the inline TangleSlider scrubs, Matrix cells edit. Their
_css files land in the right shadow root. Nothing manywidgets-specific was
required, which is the expected result: the plugin keys off anywidget, not off
manywidgets, and wigglystuff widgets are plain anywidgets whose _esm = Path(...)
gets inlined into the notebook’s metadata.widgets at execution time.
2. Links work in every direction. All seven jsdlink/jslink bindings above
were lifted into the page-level registry and re-bound in the browser —
wigglystuff → manywidgets, manywidgets → wigglystuff, wigglystuff → wigglystuff,
and jslink both ways. Dragging the Knob in 2a moves the manywidgets Stat and
NumberDisplay with it. The plugin resolves link endpoints by model_id, and it
never cared which library a model came from.
3. A manywidgets Row/Column can host a wigglystuff child (2d) — including
one that is itself a link endpoint. host.renderChild mounts any anywidget
reachable through the container’s children trait.
What to watch for¶
Python-side callbacks are inert, as with any static export. Roughly fifteen wigglystuff widgets (
LiveEdit,EnvConfig,HTMLRefreshWidget, theobserve-driven chart selectors…) do their real work in Python. Statically their JS still runs and their traits still drive links; only the Python half is dead.Six widgets fetch their JS from a CDN at render time —
Excalidraw,EsmWidget,ManimWeb,TangleLatex,ObservablePlot,AltairWidgetpull fromcdn.jsdelivr.net. They will work on a networked page but the page is no longer self-contained, and they will fail offline.GridDrawandGamepadWidgetuse space-separated event names (model.on("change:a change:b", …)). Plugin v0.2.0+ splits those, so they are fine here — but that is the one rule an anywidget author can break invisibly, since it only misbehaves without a kernel.Everything else is the usual static-export contract: serve over HTTP (not
file://), and re-runjust executeafter changing any widget code, since the JS is baked into the notebook at execution time.