Getting Started

anywidget visualizations inside a JupyterLab notebook

What is anywidget?

anywidget is a Python library that simplifies creating and publishing custom Jupyter Widgets. No messy build configuration or complicated cookiecutter templates.

It is not a new interactive widgets framework, but rather an abstraction for creating custom Jupyter Widgets using modern web standards.

Key features

  • πŸ› οΈ Create widgets without complicated cookiecutter templates
  • πŸ“š Publish to PyPI like any other Python package
  • πŸ€– Prototype within .ipynb or .py files
  • πŸš€ Run in Jupyter, JupyterLab, Google Colab, VSCode, and more
  • ⚑ Develop with instant HMR, like modern web frameworks


pip install "anywidget[dev]"
import anywidget
import traitlets

class CounterWidget(anywidget.AnyWidget):
    _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.on("change:value", () => {
        button.innerHTML = `count is ${model.get("value")}`;
    export default { render };
    _css = """
    .counter-widget button { background-color: #ea580c; }
    .counter-widget button:hover { background-color: #9a3412; }
    value = traitlets.Int(0).tag(sync=True)


What’s going on here:

  • count is a stateful property that both the client JavaScript and Python have access to. Shared state is defined via traitlets with the sync=True keyword argument.

  • _esm specifies a required ECMAScript module for the widget. It defines and exports render, a function for rendering and initializes dynamic updates for the custom widget.

  • _css specifies an optional CSS stylesheet to load for the widget. It can be a full URL or plain text. Styles are loaded in the global scope if using this feature, so take care to avoid naming conflicts.

ECMAScript modules are the offical standard format to package JavaScript code for reuse and are supported natively across all major browsers. Therefore, dependencies can be imported directly via a fully qualified URL.

import * as d3 from "";

/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
function render({ model, el }) {
	let selection =;
	/* ... */
export default { render };

The render function can also (optionally) return a callback that is executed any time the view is removed from the DOM. This feature is useful when dealing with complex event listeners, subscriptions, or third-party libraries that require proper teardown.

/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
function render({ model, el }) {
	// Create DOM elements and set up subscribers
	return () => {
		// Optionally cleanup
export default { render }

Progressive Development

As your widgets grow in complexity, it is recommended to separate your front-end code from your Python code. Just move the _esm and _css definitions to separate files and reference them via path.

import pathlib
import anywidget
import traitlets

class CounterWidget(anywidget.AnyWidget):
    _esm = pathlib.Path(__file__).parent / "index.js"
    _css = pathlib.Path(__file__).parent / "index.css"
    count = traitlets.Int(0).tag(sync=True)

This is both easier to read and allows you to use your favorite editor/IDE for your front-end code. Using file paths also enables anywidget’s built-in HMR, allowing real-time updates to your widget during development (below).

Note: Since v0.9, anywidget requires developers to opt-in to HMR using an environment variable:


or when launching a Jupyter session:

ANYWIDGET_HMR=1 jupyter lab
Real-time widget development entirely from within JupyterLab

Watch & Learn

This video provides a detailed exploration of anywidget fundementals, including synchronizing Python and JavaScript state, binary data, animations, and publishing a package on PyPI.