marimo provides a rich library of built-in UI components, but you can also create custom components using anywidget - a framework for building custom Jupyter widgets with modern web technologies.
marimo provides seamless integration with anywidget through mo.ui.anywidget():
import marimo as mofrom my_widget import MyCustomWidget# Wrap anywidget with mo.ui.anywidget()widget = mo.ui.anywidget(MyCustomWidget())# Access widget statewidget.value # Returns widget's current state as a dictionary
class anywidget(UIElement[ModelIdRef, AnyWidgetState]): """ Create a UIElement from an AnyWidget. This proxies all the widget's attributes and methods, allowing seamless integration of AnyWidget instances with marimo's UI system. """ def __init__(self, widget: AnyWidget): self.widget = widget # Widget state is synchronized automatically # with marimo's reactive system
Key components:
Python traits: Define the widget’s state (synchronized between Python and JavaScript)
JavaScript module: Renders the UI and handles interactions
marimo wrapper: Integrates with marimo’s reactive execution
# Changes in JavaScript automatically update Pythonwidget.color = '#ff0000' # Updates both Python and frontend# Access current statecurrent_state = widget.value # Dict of all traits
From examples/third_party/anywidget/tldraw_colorpicker.py:
import marimo as moimport numpy as npimport matplotlib.pyplot as pltfrom tldraw import ReactiveColorPicker# Create the widgetcolor_widget = mo.ui.anywidget(ReactiveColorPicker())# Use in visualizationfig, ax = plt.subplots()ax.scatter(x, y, s=sizes*5, color=color_widget.color or None)mo.hstack([color_widget, plt.gca()])
class CommLifecycleItem(CellLifecycleItem): """Manages widget cleanup when cells are re-executed.""" def dispose(self, context: RuntimeContext, deletion: bool) -> bool: # Close the communication channel self._comm.close() return True
marimo automatically:
Initializes widgets when cells run
Cleans up widgets when cells are re-executed or deleted
Manages communication channels between Python and JavaScript
Only sync essential state between Python and JavaScript:
# ✓ Good: Only sync what's neededclass GoodWidget(anywidget.AnyWidget): selected_id = traitlets.Unicode().tag(sync=True)# ❌ Avoid: Syncing large computed valuesclass BadWidget(anywidget.AnyWidget): full_dataset = traitlets.List().tag(sync=True) # Too much data
Handle initialization properly
Ensure your widget handles initial state correctly:
export function render({ model, el }) { // Read initial state const initialValue = model.get('value'); // Set up UI with initial state const input = createInput(initialValue); // Listen for changes from Python model.on('change:value', () => { input.value = model.get('value'); }); // Send changes to Python input.addEventListener('input', () => { model.set('value', input.value); model.save_changes(); });}