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.
npx skills add marimo-team/skills --skill anywidget-generator -a claude-code
npx skills add marimo-team/skills --skill anywidget -a claude-codeWhat 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
_esmand_cssinto 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
_esmand_cssinto separate files using pathlib - Wrap the widget in
mo.ui.anywidgetand 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-codenpx skills add marimo-team/skills --skill anywidget -a claude-code2Manually: put the files in place
~/.claude/skills/anywidget/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/anywidget/
1Skills CLI (Vercel)
npx skills add marimo-team/skills --skill anywidget -a codex2Manually: put the files in place
.agents/skills/anywidget/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add marimo-team/skills --skill anywidget -a cursor2Manually: put the files in place
.cursor/skills/anywidget/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install marimo-team/skills anywidget2Manually: put the files in place
.github/skills/anywidget/SKILL.md1Skills CLI (Vercel)
npx skills add marimo-team/skills --skill anywidget -a gemini-cli2Platform CLI
gemini skills install https://github.com/marimo-team/skills --path skills/anywidgetManually: put the files in place
.devin/skills/anywidget/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add marimo-team/skills --skill anywidget -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
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