
Marimo Notebook
- 1 installs
- 182 repo stars
- Updated August 4, 2026
- ericmjl/llamabot
This is a copy of marimo-notebook by marimo-team - installs and ranking accrue to the original listing.
Helps with productivity & planning tasks.
About
marimo-notebook is a Claude Code skill for productivity & planning. It helps solo builders move faster with AI-assisted coding.
- marimo-notebook
- Productivity & Planning
- AI-coding skill
Marimo Notebook by the numbers
- 1 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ericmjl/llamabot --skill marimo-notebookAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 182 |
| Last updated | August 4, 2026 |
| Repository | ericmjl/llamabot ↗ |
What it does
Helps with productivity & planning tasks.
Files
Notes for marimo Notebooks
marimo uses Python to create notebooks, unlike Jupyter which uses JSON. Here's an example notebook:
# /// script
# dependencies = [
# "marimo",
# "numpy==2.4.3",
# ]
# requires-python = ">=3.14"
# ///
import marimo
__generated_with = "0.20.4"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import numpy as np
return mo, np
@app.cell
def _():
print("hello world")
return
@app.cell
def _(np, slider):
np.array([1,2,3]) + slider.value
return
@app.cell
def _(mo):
slider = mo.ui.slider(1, 10, 1, label="number to add")
slider
return (slider,)
@app.cell
def _():
return
if __name__ == "__main__":
app.run()
Notice how the notebook is structured with functions can represent cell contents. Each cell is defined with the @app.cell decorator and the inputs/outputs of the function are the inputs/outputs of the cell. marimo usually takes care of the dependencies between cells automatically.
Running Marimo Notebooks
# Run as script (non-interactive, for testing)
uv run <notebook.py>
# Run interactively in browser
uv run marimo run <notebook.py>
# Edit interactively
uv run marimo edit <notebook.py>Script Mode Detection
Use mo.app_meta().mode == "script" to detect CLI vs interactive:
@app.cell
def _(mo):
is_script_mode = mo.app_meta().mode == "script"
return (is_script_mode,)Key Principle: Keep It Simple
Show all UI elements always. Only change the data source in script mode.
- Sliders, buttons, widgets should always be created and displayed
- In script mode, just use synthetic/default data instead of waiting for user input
- Don't wrap everything in
if not is_script_modeconditionals - Don't use try/except for normal control flow
Good Pattern
# Always show the widget
@app.cell
def _(ScatterWidget, mo):
scatter_widget = mo.ui.anywidget(ScatterWidget())
scatter_widget
return (scatter_widget,)
# Only change data source based on mode
@app.cell
def _(is_script_mode, make_moons, scatter_widget, np, torch):
if is_script_mode:
# Use synthetic data for testing
X, y = make_moons(n_samples=200, noise=0.2)
X_data = torch.tensor(X, dtype=torch.float32)
y_data = torch.tensor(y)
data_error = None
else:
# Use widget data in interactive mode
X, y = scatter_widget.widget.data_as_X_y
# ... process data ...
return X_data, y_data, data_error
# Always show sliders - use their .value in both modes
@app.cell
def _(mo):
lr_slider = mo.ui.slider(start=0.001, stop=0.1, value=0.01)
lr_slider
return (lr_slider,)
# Auto-run in script mode, wait for button in interactive
@app.cell
def _(is_script_mode, train_button, lr_slider, run_training, X_data, y_data):
if is_script_mode:
# Auto-run with slider defaults
results = run_training(X_data, y_data, lr=lr_slider.value)
else:
# Wait for button click
if train_button.value:
results = run_training(X_data, y_data, lr=lr_slider.value)
return (results,)State and Reactivity
Variables between cells define the reactivity of the notebook for 99% of the use-cases out there. No special state management needed. Don't mutate objects across cells (e.g., my_list.append()); create new objects instead. Avoid mo.state() unless you need bidirectional UI sync or accumulated callback state. See STATE.md for details.
Don't Guard Cells with if Statements
Marimo's reactivity means cells only run when their dependencies are ready. Don't add unnecessary guards:
# BAD - the if statement prevents the chart from showing
@app.cell
def _(plt, training_results):
if training_results: # WRONG - don't do this
fig, ax = plt.subplots()
ax.plot(training_results['losses'])
fig
return
# GOOD - let marimo handle the dependency
@app.cell
def _(plt, training_results):
fig, ax = plt.subplots()
ax.plot(training_results['losses'])
fig
returnThe cell won't run until training_results has a value anyway.
Don't Use try/except for Control Flow
Don't wrap code in try/except blocks unless you're handling a specific, expected exception. Let errors surface naturally.
# BAD - hiding errors behind try/except
@app.cell
def _(scatter_widget, np, torch):
try:
X, y = scatter_widget.widget.data_as_X_y
X = np.array(X, dtype=np.float32)
# ...
except Exception as e:
return None, None, f"Error: {e}"
# GOOD - let it fail if something is wrong
@app.cell
def _(scatter_widget, np, torch):
X, y = scatter_widget.widget.data_as_X_y
X = np.array(X, dtype=np.float32)
# ...Only use try/except when:
- You're handling a specific, known exception type
- The exception is expected in normal operation (e.g., file not found)
- You have a meaningful recovery action
Cell Output Rendering
Marimo only renders the final expression of a cell. Indented or conditional expressions won't render:
# BAD - indented expression won't render
@app.cell
def _(mo, condition):
if condition:
mo.md("This won't show!") # WRONG - indented
return
# GOOD - final expression renders
@app.cell
def _(mo, condition):
result = mo.md("Shown!") if condition else mo.md("Also shown!")
result # This renders because it's the final expression
returnPEP 723 Dependencies
Notebooks created via marimo edit --sandbox have these dependencies added to the top of the file automatically but it is a good practice to make sure these exist when creating a notebook too:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# "torch>=2.0.0",
# ]
# ///marimo check
When working on a notebook it is important to check if the notebook can run. That's why marimo provides a check command that acts as a linter to find common mistakes.
uvx marimo check <notebook.py>Make sure these are checked before handing a notebook back to the user.
api docs
If the user specifically wants you to use a marimo function, you can locally check the docs via:
uv --with marimo run python -c "import marimo as mo; help(mo.ui.form)"tests
By default, marimo discovers and executes tests inside your notebook. When the optional pytest dependency is present, marimo runs pytest on cells that consist exclusively of test code - i.e. functions whose names start with test_. If the user asks you to add tests, make sure to add the pytest dependency is added and that there is a cell that contains only test code.
For more information on testing with pytest see PYTEST.md
Once tests are added, you can run pytest from the commandline on the notebook to run pytest.
pytest <notebook.py>Additional resources
- For SQL use in marimo see SQL.md
- For UI elements in marimo UI.md
- For exposing functions/classes as top level imports TOP-LEVEL-IMPORTS.md
- For exporting notebooks (PDF, HTML, markdown, etc.) EXPORTS.md
- For state management and reactivity STATE.md
- For deployment of marimo notebooks DEPLOYMENT.md
- For custom interactive widgets with anywidget ANYWIDGET.md
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
renderfunction that takes{ model, el }as parameters - Use
model.get()to read trait values - Use
model.set()andmodel.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, sowidget.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.valuewhich 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.
Running notebooks
You can deploy a single marimo notebook as a web app:
uvx marimo run --sandbox notebook.pyThe --sandbox flag makes sure the notebook runs in an isolated UV environment.
Or deploy a folder of notebooks as a web app with multiple notebooks. Also here, you can use the --sandbox flag to run each notebook in its own isolated environment, using the PEP 723 dependencies declared in each notebook:
uvx marimo run --sandbox <folder>Thumbnails
When you host multiple notebooks you may want to generate thumbnails. You can generate OpenGraph thumbnails for notebooks using:
uvx marimo export thumbnail notebook.py
uvx marimo export thumbnail folder/Thumbnails are stored at __marimo__/assets/<notebook_stem>/opengraph.png. The user may also put screenshots there manually.
Besides images, you can also add metadata to the notebooks by adding to the PEP 723 Dependencies on top of the file. These will appear in an overview if the user deploys a folder of notebooks.
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# "polars==1.37.1",
# ]
# [tool.marimo.opengraph]
# title = "My dashboard"
# description = "Tracking my portfolio over time"
# ///marimo can export notebooks to several formats via the CLI.
> uvx marimo export --help
Usage: marimo export [OPTIONS]
COMMAND [ARGS]...
Export a notebook to various formats.
Options:
-h, --help Show this message and exit.
Commands:
html Run a notebook and export it as an HTML file.
html-wasm Export a notebook as a WASM- powered marimo notebook.
ipynb Export a marimo notebook as a Jupyter notebook
md Export a marimo notebook as a code fenced markdown file
pdf Export a marimo notebook as a PDF file.
script Export a marimo notebook as a flat script
session Execute a notebook or directory of notebooks and export session snapshots.
thumbnail Generate OpenGraph thumbnails for notebooks.You can learn more about each option by calling the command with the --help flag.
PDF Export
Many people may be interested in exporting to a PDF.
uvx marimo export pdf notebook.py -o notebook.pdfPDF export uses nbformat and nbconvert under the hood. By default it uses the WebPDF exporter which requires Chromium. Install the dependencies:
uv pip install nbformat nbconvert
playwright install chromiumUseful flags:
--no-include-inputs— hide code cells, show only outputs--no-include-outputs— include only code, skip outputs--as=slides— export as a slide deck PDF (uses reveal.js slide boundaries)--raster-scale 4.0— controls output sharpness (1.0–4.0, default 4.0)--raster-server=live— use when a widget needs a running Python kernel to render (recommended for slides)
Script Export
uvx marimo export script notebook.py -o notebook.script.pyFlattens the notebook into a plain Python script in topological order.
Common Flags
These flags work across most export subcommands:
-o,--output— output file path--watch— re-export automatically when the notebook file changes--sandbox— run in an isolateduvenvironment-f,--force— overwrite if output file already exists--— pass CLI arguments to the notebook, e.g.uvx marimo export html notebook.py -o out.html -- --arg value-yautomatic yes to prompts on the terminaluvx marimo -y CMD ...
Testing with pytest
Testing in notebook
When pytest is present, marimo runs pytest on cells that consist exclusively of test code - i.e. functions whose names start with test_, classes whose names start with Test, or functions decorated with @pytest.fixture. If a cell mixes in anything else (helper functions, constants, variables, imports, etc.), that cell is skipped by the test runner (we recommend you move helpers to another cell).
For example:
@app.cell
def __():
import pytest
def inc(x):
return x + 1
return inc, pytest
@app.cell
def __(inc, pytest):
class TestBlock:
@staticmethod
def test_fails():
assert inc(3) == 5, "This test fails"
@staticmethod
def test_sanity():
assert inc(3) == 4, "This test passes"
@pytest.mark.parametrize(("x", "y"), [(3, 4), (4, 5)])
def test_parameterized(x, y):
assert inc(x) == y
returnReactive tests can be disabled. You can disable this behavior with the runtime.reactive_test option in the configuration file.
Testing at the command-line
pytest <notebook.py>runs and tests all notebook cells whose names start with test_, or cells that contain only test_ functions and Test classes (just like in notebook tests).
Example
Running pytest on
# content of test_notebook.py
import marimo
__generated_with = "0.10.6"
app = marimo.App()
@app.cell
def _():
def inc(x):
return x + 1
return (inc,)
@app.cell
def test_fails(inc):
assert inc(3) == 5, "This test fails"
@app.cell
def test_sanity(inc):
assert inc(3) == 4, "This test passes"
@app.cell
def collection_of_tests(inc, pytest):
@pytest.mark.parametrize(("x", "y"), [(3, 4), (4, 5)])
def test_answer(x, y):
assert inc(x) == y, "These tests should pass."
@app.cell
def imports():
import pytest
return pytestprints
============================= test session starts ==============================
platform linux -- Python 3.12.9, pytest-8.3.5, pluggy-1.5.0
rootdir: /notebooks
configfile: pyproject.toml
collected 4 items
test_notebook.py::test_fails FAILED [ 25%]
test_notebook.py::test_sanity PASSED [ 50%]
test_notebook.py::MarimoTestBlock_0::test_parameterized[3-4] PASSED [ 75%]
test_notebook.py::MarimoTestBlock_0::test_parameterized[4-5] PASSED [100%]
=================================== FAILURES ===================================
__________________________________ test_fails __________________________________
# content of test_notebook.py
import marimo
__generated_with = "0.10.6"
app = marimo.App()
@app.cell
def _():
def inc(x):
return x + 1
return (inc,)
@app.cell
def test_fails(inc):
> assert inc(3) == 5, "This test fails"
E AssertionError: This test fails
E assert 4 == 5
E + where 4 = <function inc>(3)
test_notebook.py:17: AssertionError
=========================== short test summary info ============================
FAILED test_notebook.py::test_fails - AssertionError: This test fails
========================= 1 failed, 3 passed in 0.82s ==========================Using Pytest Fixtures
marimo supports pytest fixtures, with one limitation: fixtures defined in one cell cannot be used in another cell, unless the fixtures were defined in the setup cell.
Fixtures defined in the setup cell:
# test_notebook.py
import marimo
app = marimo.App()
with app.setup:
from fixtures import db_connection, sample_data
@app.cell
def _(sample_data):
def test_data_loaded(sample_data):
assert len(sample_data) > 0Fixtures in the same cell as tests:
@app.cell
def _():
import pytest
return pytest
@app.cell
def _(pytest):
@pytest.fixture
def temp_file():
import tempfile
with tempfile.NamedTemporaryFile() as f:
yield f
def test_writes_to_file(temp_file):
temp_file.write(b"hello")
temp_file.seek(0)
assert temp_file.read() == b"hello"Class fixtures:
@app.cell
def _():
import pytest
return pytest
@app.cell
def _(pytest):
class TestDatabase:
@pytest.fixture(scope="class")
def connection(self):
return create_connection()
def test_query(self, connection):
result = connection.query("SELECT 1")
assert result == 1`conftest.py` fixtures work as expected - pytest discovers them automatically.
Fixture Limitations
Fixtures defined in one cell cannot be used by tests in a different cell. This is because pytest collects tests statically by parsing the notebook file without executing it. During collection, pytest can see module-level fixtures (from conftest.py or imported modules) and fixtures defined in the same scope as the test, but it cannot see fixtures defined in other cells.
Why? Running the entire notebook just for fixture discovery would be expensive, and static analysis cannot determine which fixtures will be available after cell execution since cell order is determined at runtime by marimo's dependency graph.
There are multiple ways to use SQL in marimo. Under the hood, a SQL cell is just a function call to marimo.sql. A cell looks like this:
@app.cell(hide_code=True)
def _(df, mo):
grouped = mo.sql(
f"""
SELECT category, AVG(value) as mean FROM df GROUP BY category ORDER BY mean;
""",
output=False
)
return (grouped,)grouped is a polars dataframe. By defauly marimo uses DuckDB in memory and can refer to dataframe variables that are in scope.
This is what the signature is of mo.sql:
def sql(query: str, *, output: bool=True, engine: Optional[DBAPIConnection]=None) -> AnyTypically a sql call returns a polars dataframe, but the user can configure pandas as an alternative.
Notice how a query string goes in with SQL and how you can pass a specific database engine. Be aware that different SQL engines may have different SQL dialects.
SQLAlchemy
One possible engine is SQLAlchemy.
import sqlalchemy
# Create an in-memory SQLite database with SQLAlchemy
sqlite_engine = sqlalchemy.create_engine("sqlite:///:memory:")You can also use SQLModel with a similar connection string.
DuckDB
You can also use DuckDB with a connection string.
import duckdb
# Create a DuckDB connection
duckdb_conn = duckdb.connect("file.db", read_only=True)PyIceberg
marimo supports data catalogs as well.
from pyiceberg.catalog.rest import RestCatalog
catalog = RestCatalog(
name="catalog",
warehouse="1234567890",
uri="https://example.com",
token="my-token",
)State in marimo
Reactivity IS State Management
In marimo, regular Python variables between cells are your state. When a cell assigns a variable, all cells that read it re-run automatically. Widget values (widget.value) work the same way — interact with a widget and dependent cells re-execute. No store, no session_state, no hooks needed.
Don't Mutate Objects Across Cells
marimo does not track mutations like my_list.append(42) or obj.value = 42.
# BAD - mutation in another cell won't trigger re-runs
# Cell 1
items = [1, 2, 3]
# Cell 2
items.append(4) # marimo won't know this happened
# GOOD - create new objects instead
# Cell 1
items = [1, 2, 3]
# Cell 2
extended_items = items + [4]You Probably Don't Need mo.state()
In 99% of cases, built-in reactivity is enough:
- Reading widget values — just use
widget.valuein another cell - Combining multiple inputs — use
.batch().form() - Conditional data — use
if/elsein one cell
When You Do Need mo.state()
Use it when you need accumulated state from callbacks or bidirectional sync between UI elements.
get_val, set_val = mo.state(initial_value)- Read:
get_val() - Update:
set_val(new_value)orset_val(lambda d: d + [new_item]) - The cell calling the setter does NOT re-run (unless
allow_self_loops=True)
Example: todo list with accumulated state
# Cell 1 — declare state
@app.cell
def _(mo):
get_items, set_items = mo.state([])
return get_items, set_items
# Cell 2 — input form
@app.cell
def _(mo, set_items):
task = mo.ui.text(label="New task")
add = mo.ui.button(
label="Add",
on_click=lambda _: set_items(lambda d: d + [task.value])
)
mo.hstack([task, add])
return
# Cell 3 — display (re-runs when state changes)
@app.cell
def _(mo, get_items):
mo.md("\n".join(f"- {t}" for t in get_items()))
returnExample: syncing two UI elements
@app.cell
def _(mo):
get_n, set_n = mo.state(50)
return get_n, set_n
@app.cell
def _(mo, get_n, set_n):
slider = mo.ui.slider(0, 100, value=get_n(), on_change=set_n)
number = mo.ui.number(0, 100, value=get_n(), on_change=set_n)
mo.hstack([slider, number])
returnWarnings
- Don't store
mo.uielements inside state — causes hard-to-diagnose bugs. - Don't use
on_changewhen you can just read.valuefrom another cell. - Write idempotent cells — same inputs should produce same outputs.
You can import top-level functions and classes defined in a marimo notebook into other Python scripts or notebooks using normal Python syntax, as long as your definitions satisfy the simple criteria described on this page. This makes your notebook code reusable, testable, and easier to edit in text editors of your choice.
For a function or class to be saved at the top level of the notebook file, it must meet the following criteria:
The cell must define just a single function or class. The defined function or class can only refer to symbols defined in the setup cell, or to other top-level symbols.
# /// script
# dependencies = [
# "marimo",
# "numpy==2.4.2",
# ]
# requires-python = ">=3.14"
# ///
import marimo
__generated_with = "0.19.11"
app = marimo.App(width="medium")
# Define setup cell
with app.setup:
import numpy as np
# Define function cell
@app.function
def calculate_statistics(data):
"""Calculate basic statistics for a dataset"""
return {
"mean": np.mean(data),
"median": np.median(data),
"std": np.std(data)
}
@app.cell
def _():
import marimo as mo
return
if __name__ == "__main__":
app.run()In this example, the setup cell is represented as a context manager app.setup and the cell that contains calculate_statistics is represented as a function decorator @app.function. You can now import calculate_statistics from other Python scripts or notebooks. There can be no more than one setup cell per notebook.
# In another_script.py
from my_notebook import calculate_statistics
data = [1, 2, 3, 4, 5]
stats = calculate_statistics(data)
print(stats)marimo has a rich set of UI components.
mo.ui.altair_chart(altair_chart)- create a reactive Altair chartmo.ui.button(value=None, kind='primary')- create a clickable buttonmo.ui.run_button(label=None, tooltip=None, kind='primary')- create a button that runs codemo.ui.checkbox(label='', value=False)- create a checkboxmo.ui.chat(placeholder='', value=None)- create a chat interfacemo.ui.date(value=None, label=None, full_width=False)- create a date pickermo.ui.dropdown(options, value=None, label=None, full_width=False)- create a dropdown menumo.ui.file(label='', multiple=False, full_width=False)- create a file upload elementmo.ui.number(value=None, label=None, full_width=False)- create a number inputmo.ui.radio(options, value=None, label=None, full_width=False)- create radio buttonsmo.ui.refresh(options: List[str], default_interval: str)- create a refresh controlmo.ui.slider(start, stop, value=None, label=None, full_width=False, step=None)- create a slidermo.ui.range_slider(start, stop, value=None, label=None, full_width=False, step=None)- create a range slidermo.ui.table(data, columns=None, on_select=None, sortable=True, filterable=True)- create an interactive tablemo.ui.text(value='', label=None, full_width=False)- create a text inputmo.ui.text_area(value='', label=None, full_width=False)- create a multi-line text inputmo.ui.data_explorer(df)- create an interactive dataframe explorermo.ui.dataframe(df)- display a dataframe with search, filter, and sort capabilitiesmo.ui.plotly(plotly_figure)- create a reactive Plotly chart (supports scatter, treemap, and sunburst)mo.ui.tabs(elements: dict[str, mo.ui.Element])- create a tabbed interface from a dictionarymo.ui.array(elements: list[mo.ui.Element])- create an array of UI elementsmo.ui.form(element: mo.ui.Element, label='', bordered=True)- wrap an element in a form
As always, you can learn more about the available inputs to all these components via uv --with marimo run python -c "import marimo as mo; help(mo.ui.form)"
Forms
You can compose multiple UI elements into a single form using .batch().form(). The .batch() method binds named UI elements into a markdown template, and .form() adds a submit button so values are only sent on submit.
form = (
mo.md(
"""
**Choose an option**
{choice}
**Enter some text**
{text}
**Enable feature**
{flag}
"""
)
.batch(
choice=mo.ui.dropdown(options=["A", "B", "C"]),
text=mo.ui.text(),
flag=mo.ui.checkbox(),
)
.form(
submit_button_label="Submit",
show_clear_button=True, # optional
clear_on_submit=False, # keep values after submit
)
)
formYou can also add validation to a form using the validate parameter. Return an error string to block submission, or None to allow it.
group_by_form = mo.ui.dropdown(
options=df_columns,
label="Select column to filter for duplicate analyzis",
allow_select_none=True,
value=None, # start with nothing selected
searchable=True,
).form(
submit_button_label="Apply",
validate=lambda v: (
"Please select a column and press Apply."
if v is None else None
),
)However, the user may also want to use other components. Popular alternatives include the ScatterWidget from the drawdata library, moutils, and wigglystuff.
For custom classes and static HTML representations you can also use the _display_ method.
class Dice:
def _display_(self):
import random
return f"You rolled {random.randint(0, 7)}"