Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Create your own widget

Every manywidgets widget follows the same self-contained structure, so the fastest way to make a new one is to copy an existing widget directory and rename it. A widget is a complete unit: Python class, TypeScript source, built bundle, styles, and its own tests.

1. Copy a widget directory

cp -r src/manywidgets/slider src/manywidgets/my_widget

You get:

src/manywidgets/my_widget/
├── __init__.py        # re-export the class
├── widget.py          # the BaseWidget subclass (traits + methods)
├── doc.md             # the widget's docs (auto-assembled into the docs site)
├── src/index.ts       # the render() function
├── style.css          # styling via the _css trait
├── dist/widget.js     # built by esbuild (gitignored)
└── tests/
    ├── test_my_widget.py    # pytest
    └── my_widget.test.ts    # vitest + jsdom

Rename the class in widget.py and __init__.py, update tests/ and doc.md, then add the class to src/manywidgets/__init__.py and to the ensured-targets / skip-if-exists lists in pyproject.toml. The build (scripts/build.mjs) auto-discovers any widget dir with a src/index.ts.

Docs (doc.md) — co-located and auto-assembled

A widget owns its docs. scripts/build_widget_docs.py turns each doc.md into docs/widgets/<name>.ipynb (a gitignored build artifact) and the MyST site picks it up. In doc.md:

Add the new page to the widgets: toc in docs/myst.yml. Regenerate with npm run docs:gen.

2. The golden-example pattern

3. The static-export safety rules

manywidgets widgets render both in a live kernel and statically (no kernel) via the myst-anywidget-static-export plugin. To stay compatible:

  1. Wrap every model.save_changes() — use safeSaveChanges(model) from core (there is no kernel statically).

  2. One listener per trait. Use onChanges(model, ["a", "b"], fn) — never model.on("change:a change:b", fn). The static model emitter does not split space-separated event names, so the combined form silently never fires. (This is the single most common static-export bug; see static export.)

  3. Style via the _css trait (or ensureShadowCss from core for libraries that inject CSS at runtime) — never append a <link> into the mount el.

  4. Vanilla DOM only — no createRoot(el) that wipes shadow children.

  5. Stay buffer-free for core widgets (JSON-serialisable traits). Binary traits need an nbclient pre-execute step on export.

  6. Cross-widget access via resolveModel from core — it resolves root widgets by widget_id and fans writes out to sub-model proxies, handling the live-kernel and static-export cases.

3b. Container widgets (laying out children)

A widget can render other widgets inside its own DOM — that’s how Row, Column, and Grid work. Two pieces:

Children keep their own JS, CSS, and links. Requires plugin v0.2.0+ for static export. Test with the fakeHost() helper from @manywidgets/test-utils.

4. Tests

Write both:

Run them: pytest and npm test.