anywidget: Jupyter widgets without the cookiecutter, plus an npm and JSR toolchain
reusable widgets made easy
At a glance
- What is it?
- anywidget is a specification and a Python toolkit for writing reusable web widgets that run in Jupyter, JupyterLab, Colab, VSCode and marimo, built on ipywidgets and traitlets. The Python package is only one piece of a monorepo that also ships a Vite plugin, React and Svelte bridges, a bootstrap CLI, and JSR packages for Deno and signals.
- Who is it for?
- anywidget suits Python developers who need a reusable interactive widget and want to ship it as an ordinary PyPI package instead of maintaining a JupyterLab extension, and it is the wrong tool if your widget needs to be a full application rather than a component embedded in a notebook, or if you need to support Python below 3.10, which is the floor the package declares.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A widget specification first, a Python package second
anywidget is described in the README as both a specification and a toolkit for authoring reusable web based widgets for interactive computing environments. That ordering matters. The specification is the part that lets a widget written one way run in several hosts, and the toolkit is the Python surface that implements it.
The problem being solved is packaging friction. Historically, a reusable Jupyter widget was distributed as a JupyterLab extension, a JavaScript bundle plus a Python package, with its own build chain and its own release choreography. The README lists the benefits as creating custom Jupyter Widgets without complicated cookiecutter templates, publishing to PyPI like any other Python package, prototyping within `.ipynb` or `.py` files, and running in Jupyter, JupyterLab, Google Colab, VSCode and marimo.
The runtime dependencies make the mechanism visible. `ipywidgets>=7.6.0` provides the widget protocol and the traitlet state model, `psygnal>=0.8.1` provides the event system, and `typing-extensions>=4.2.0` is there for type hints on supported Python versions. The package requires Python 3.10 or newer and is built with hatchling, and the classifiers list both JupyterLab 4 and the prebuilt JupyterLab extension style, which reflects that it supports both classic notebook and JupyterLab 4 hosts.
The audience is Python developers who need an interactive component in a notebook or an analysis script and want to reuse it later. If you are not building something that someone else will import, the specification half of the project is not for you.
Two class attributes carry the whole widget contract
The core mechanism is small enough to state in a sentence: a widget is a Python class whose front-end code lives in the `_esm` attribute and whose optional styles live in `_css`, with traitlets holding the state that both sides read.
The README's first example inlines the JavaScript directly:
import anywidget
import traitlets
class CounterWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
function render({ model, el }) {
let button = document.createElement("button");
button.innerHTML = `count is ${model.get("value")}`;
button.addEventListener("click", () => {
model.set("value", model.get("value") + 1);
model.save_changes();
});
model.on("change:value", () => {
button.innerHTML = `count is ${model.get("value")}`;
});
el.appendChild(button);
}
export default { render };
"""
# Stateful property that can be accessed by JavaScript & Python
value = traitlets.Int(0).tag(sync=True)Three mechanisms are worth pulling out. The `render` function receives the widget model and the element to mount into, and it exports as a default export, which is the standard ES module entry contract. The `model.get`, `model.set` and `model.save_changes` calls are the sync protocol, and `model.save_changes()` is what pushes a Python-side change out to the browser, since without it a `model.set` is local until saved. The `model.on("change:value", ...)` handler is the reverse direction, keeping the DOM in step when the value changes from Python.
The traitlet is where the synchronisation boundary is declared. `traitlets.Int(0).tag(sync=True)` marks the value as shared state, so it is transmitted both ways, and the type annotation is not decoration: the traitlet type is what constrains the value.
The recommended layout keeps front-end code in real files
The README is direct about the recommended shape of a real widget, and it differs from the toy example. Instead of a string, `_esm` and `_css` take paths:
import pathlib
import anywidget
import traitlets
class CounterWidget(anywidget.AnyWidget):
_esm = pathlib.Path("index.js")
_css = pathlib.Path("styles.css")
value = traitlets.Int(0).tag(sync=True)The benefit is that `index.js` and `styles.css` become ordinary files that an editor can complete, a linter can check and a bundler can process. That is the point of the frontend tooling in the monorepo. A Vite plugin, `@anywidget/vite`, exists so that a front-end build can be wired into a widget, and the README advertises instant hot module replacement with modern web frameworks as a development benefit.
The cost is a build step and a set of conventions. A path-based `_esm` means the file has to exist relative to the module at runtime, which means packaging has to include it. The pyproject configuration addresses this explicitly: the hatch build excludes `.github`, `docs` and `paper`, and lists artifacts including `anywidget/nbextension/index.*` and `anywidget/labextension/*.tgz`, with shared data mapping the nbextension into `share/jupyter/nbextensions/anywidget`, the labextension into `share/jupyter/labextensions/anywidget`, and `anywidget.json` into `etc/jupyter/nbconfig/notebook.d/anywidget.json`. A Jupyter build hook, `hatch_jupyter_builder.npm_builder`, ensures those targets exist at build time.
So the Python package ships both the Python code and the front-end build output, and the Jupyter side receives it through the standard extension directories.
One monorepo, eight packages across three registries
The project is a pnpm workspace with a `packages/` directory and a `pnpm-workspace.yaml`, and the package table in the README lists what is inside it. The primary Python package is `anywidget`. The npm side holds `@anywidget/types` for client type declarations, `@anywidget/vite` as a Vite plugin, `@anywidget/react` as a React framework bridge, `@anywidget/svelte` as a Svelte framework bridge, and `create-anywidget` as a CLI to bootstrap a new project. The JSR side holds `@anywidget/deno`, described as a backend for the Deno Jupyter kernel, and `@anywidget/signals`, a signals bridge.
Three registries for one project is unusual, and it maps to three different consumers. The Python package serves notebook users. The npm packages serve front-end developers who want typed client code, a build plugin, or a framework binding. The JSR packages serve the Deno ecosystem, where the Jupyter kernel is not Python, so a pure Python distribution cannot reach it.
The framework bridges deserve a note. A React or Svelte bridge lets a widget be built as a normal component in that framework's own toolchain rather than as a hand-written `render` function, which is a real advantage for teams whose front-end conventions are already set. The signals bridge suggests the same idea for reactive signal based front ends.
The release tooling in the root `package.json` shows how the three outputs are published together. The release script runs `pnpm build:packages`, then `uv build`, then `changeset publish`, then `uvx twine upload --skip-existing dist/*`. Changesets drive the JavaScript versions, uv builds the Python package, and twine uploads it, with the skip-existing flag so reruns do not fail on an artifact already on PyPI. Tests run through `vp test` with vitest and a Playwright browser provider, which means widget behaviour is exercised in a real browser rather than only in a mocked DOM.
HMR, prototype in place, and the tradeoff of the two file model
The README lists prototype within `.ipynb` or `.py` files and instant hot module replacement among the benefits. The combination is the interesting part, because it removes the usual reason people reach for the classic widget template: the edit refresh loop.
The usual workflow for a Jupyter widget extension is edit JavaScript, rebuild the bundle, reinstall the package, reload the browser. The path based `_esm` layout plus a Vite plugin shortens that to edit the file, and the browser updates. That is a development-time argument, but it is the argument that decides whether people actually build reusable widgets or abandon the idea after a weekend.
The tradeoff is that the two file model is a convention you must keep. If a contributor edits the inline string form while the rest of the project uses paths, the widget still works, but the development loop changes for that file. And once you have a `vite.config.ts` in the repository, the project is no longer a pure Python package, so contributors need node tooling as well as a Python environment. The root package.json confirms the scale of that: a pnpm workspace at version 11.18.0, rspack for building the lab extension, publint for checking package publishing correctness, and TypeScript build across the packages.
That is the cost side of the promise. A single Python package with a widget class is genuinely simple, but a package you intend to publish and maintain long term is a two language project with a release pipeline. The README is honest that this is what the ecosystem around it is for, and the presence of a `CONTRIBUTING.md`, a `rfcs/` directory and a `.changeset/` directory shows a project organised for outside contributors.
Version churn across packages, and what to pin
The release history shows the three outputs moving at different speeds. The most recent tagged release listed is `@anywidget/[email protected]` from 2026-07-31. The Python package `[email protected]` and the Vue bridge `@anywidget/[email protected]` are both dated 2026-04-27.
Two things follow. First, the Python package and the JavaScript packages are versioned independently, which is correct for a specification that spans ecosystems, but it means there is no single version number to quote for the project as a whole. When you pin, you pin the Python package for the runtime and the npm or JSR packages for the build. Second, the pre-1.0 versions of the framework bridges and the Deno and signals packages signal early API surface. A bridge at 0.1.x is not something to build a long lived product on without expecting churn.
The Python package at 0.11.0 is itself pre-1.0, though it is past the stage where the API is moving weekly. The changelogs live alongside each package in `packages/`, and the repository carries a `CHANGELOG.md` at the root as well, so the history is accessible without digging through release notes.
On maintenance, the last push was on 2026-10-02, which is recent. The repository also carries a `CITATION.cff` file and a `paper/` directory, and the README asks you to cite the Journal of Open Source Software paper at DOI 10.21105/joss.06939 describing the overall project and its vision. The licence is MIT, stated in the pyproject metadata and in the classifiers.
Support is community based: a Discord server linked from the README and GitHub Discussions for questions that do not fit an issue.
Editorial conclusion
anywidget suits Python developers who need a reusable interactive widget and want to ship it as an ordinary PyPI package instead of maintaining a JupyterLab extension, and it is the wrong tool if your widget needs to be a full application rather than a component embedded in a notebook, or if you need to support Python below 3.10, which is the floor the package declares. Verify three things before you commit: that the environments you target are on the list the README names, that you are happy with the two file model of front-end code, since the recommended layout keeps JavaScript and CSS in separate files loaded by path, and that the `_esm` and `_css` attributes survive your build tool, because the widget contract is defined by those two class attributes rather than by any framework.
Frequently asked questions
How do I install anywidget?
From PyPI with `pip install "anywidget[dev]"`, or from conda-forge with `conda install -c conda-forge anywidget`. The package requires Python 3.10 or newer.
What is the difference between anywidget and a classic Jupyter widget?
anywidget is both a specification and a toolkit, and the point of the specification is that a widget can be published to PyPI like any other Python package rather than shipped as a JupyterLab extension. The front-end code lives in the `_esm` attribute and the state in traitlets tagged with `sync=True`.
Which environments can an anywidget run in?
The README lists Jupyter, JupyterLab, Google Colab, VSCode and marimo. The package classifiers also target JupyterLab 4 and prebuilt JupyterLab extensions.
Can I write the widget front-end in React or Svelte?
Yes. The monorepo publishes `@anywidget/react` as a React framework bridge and `@anywidget/svelte` as a Svelte framework bridge, alongside the `@anywidget/vite` plugin for builds and `create-anywidget` as a CLI to bootstrap a new project.
Does anywidget work outside the Python Jupyter kernel?
The JSR package `@anywidget/deno` is described as a backend for the Deno Jupyter kernel, and `@anywidget/signals` provides a signals bridge for front ends built on reactive signals.
How does anywidget synchronize state between Python and JavaScript?
State is held in traitlets, and a property tagged with `sync=True` is shared both ways. On the JavaScript side the widget uses `model.get` and `model.set` for reads and writes, and `model.save_changes()` pushes a change out to the browser.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/manzt-anywidget)Was this useful?