Skip to main content
skill CX Codex

anywidget-generator — Claude Code skill: marimo anywidgets

A skill that steers the agent through building custom anywidget components for marimo notebooks: JavaScript in _esm, styles in _css, wrapped with mo.ui.anywidget.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add marimo-team/skills --skill anywidget-generator -a claude-code
npx skills add marimo-team/skills --skill anywidget -a claude-code
Open on GitHub

What anywidget-generator does

The skill sets the ground rules for generating a marimo widget. Vanilla JavaScript goes in _esm, a _css block is expected, and display always goes through widget = mo.ui.anywidget(OriginalAnywidget()). Read values back with widget.value, which returns a dictionary.

Inside _esm you define render({ model, el }): read traits with model.get(), write them with model.set() plus model.save_changes(), and react to updates with model.on("change:traitname", callback). The file has to end with export default { render };. Styling should stay small and look deliberate in both light and dark mode, using @media (prefers-color-scheme: dark). Once the JavaScript or CSS grows, _esm and _css can point at external files via pathlib, with paths relative to the project directory. Reading files outside the project — ~/.ssh, ~/.env, /etc/ — and embedding their contents in widget output is off limits.

Examples should stay minimal: show the core usage and skip mixing the widget with other marimo UI elements unless asked. The overall stance is that dumber is better — plain code someone can read top to bottom beats clever abstraction.

What it helps with

  • build an interactive widget for a marimo notebook without hand-rolling the boilerplate
  • pull trait values into another cell through widget.value
  • react to a trait change and refresh the UI
  • move grown _esm and _css into separate project files
  • keep widget styles readable in dark mode

Example prompts for the agent

  • Build an anywidget with a slider and a label, put the styling in _css
  • Move this widget's _esm and _css into separate files using pathlib
  • Wrap the widget in mo.ui.anywidget and show how to read its value from another cell

How to install anywidget-generator

1Skills CLI (Vercel)

npx skills add marimo-team/skills --skill anywidget-generator -a claude-code
npx skills add marimo-team/skills --skill anywidget -a claude-code

2Manually: put the files in place

~/.claude/skills/anywidget/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/anywidget/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about anywidget-generator

How do I install anywidget-generator in Claude?

Run npx skills add marimo-team/skills --skill anywidget-generator -a claude-code. The skill lands at ~/.claude/skills/anywidget/SKILL.md.

What does anywidget-generator do?

It guides the agent through writing anywidget components for marimo: the _esm structure in vanilla JavaScript, _css for light and dark mode, and the mo.ui.anywidget wrapper.

Can the widget's JavaScript and CSS live in separate files?

Yes, the skill allows pointing _esm and _css at external files through pathlib. Keep those paths relative to the project directory, for instance with Path(__file__).

Source text

SKILL.md · Apache-2.0
---
name: anywidget-generator
description: Generate anywidget components for marimo notebooks.
---

When writing an anywidget use vanilla javascript in `_esm` and do not forget about `_css`. The css should look bespoke in light mode and dark mode. Keep the css small unless explicitly asked to go the extra mile. When you display the widget it must be wrapped via `widget = mo.ui.anywidget(OriginalAnywidget())`. You can also point `_esm` and `_css` to external files if needed using pathlib. This makes sense if the widget does a lot of elaborate JavaScript or CSS.

<example title="Example of simple anywidget implementation">
import anywidget
import traitlets


class CounterWidget(anywidget.AnyWidget):
    _esm = """
    // Define the main render function
    function render({ model, el }) {
      let count = () => model.get("number");
      let btn = document.createElement("b8utton");
      btn.innerHTML = `count is ${count()}`;
      btn.addEventListener("click", () => {
        model.set("number", count() + 1);
        model.save_changes();
      });
      model.on("change:number", () => {
        btn.innerHTML = `count is ${count()}`;
      });
      el.appendChild(btn);
    }
    // Important! We must export at the bottom here!
    export default { render };
    """
    _css = """button{
      font-size: 14px;
    }"""
    number = traitlets.Int(0).tag(sync=True)

widget = mo.ui.anywidget(CounterWidget())
widget

# Grabbing the widget from another cell, `.value` is a dictionary.
print(widget.value["number"])
</example>

The above is a minimal example that could work for a simple counter widget. In general the widget can become much larger because of all the JavaScript and CSS required. Unless the widget is dead simple, you should consider using external files for `_esm` and `_css` using pathlib. 

When sharing the anywidget, keep the example minimal. No need to combine it with marimo ui elements unless explicitly stated to do so.

## Best Practices

Unless specifically told otherwise, assume the following:

1. **Use vanilla JavaScript in `_esm`**:
   - Define a `render` function that takes `{ model, el }` as parameters
   - Use `model.get()` to read trait values
   - Use `model.set()` and `model.save_changes()` to update traits
   - Listen to changes with `model.on("change:traitname", callback)`
   - Export default with `export default { render };` at the bottom
   - All widgets inherit from `anywidget.AnyWidget`, so `widget.observe(handler)`
     remains the standard way to react to state changes.
   - Python constructors tend to validate bounds, lengths, or choice counts; let the
     raised `ValueError/TraitError` guide you instead of duplicating the logic.

2. **Include `_css` styling**:
   - Keep CSS minimal unless explicitly asked for more
   - Make it look bespoke in both light and dark mode
   - Use CSS media query for dark mode: `@media (prefers-color-scheme: dark) { ... }`

3. **Wrap the widget for display**:
   - Always wrap with marimo: `widget = mo.ui.anywidget(OriginalAnywidget())`
   - Access values via `widget.value` which returns a dictionary

4. **Keep examples minimal**:
   - Add a marimo notebook that highlights the core utility
   - Show basic usage only
   - Don't combine with other marimo UI elements unless explicitly requested

5. **External file paths**: When using pathlib for external `_esm`/`_css` files, keep paths relative to the project directory, consider using `Path(__file__)` for this. Do not read files outside the project (e.g., `~/.ssh`, `~/.env`, `/etc/`) or embed their contents in widget output.

Dumber is better. Prefer obvious, direct code over clever abstractions—someone
new to the project should be able to read the code top-to-bottom and grok it
without needing to look up framework magic or trace through indirection.

© marimo-team · Apache-2.0 · marimo-team/skills

Similar skills

All skills for Data and databases →

From the same set8

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor