skills/streamlit-to-marimo/SKILL.md
Convert a Streamlit app to a marimo notebook
npx skillsauth add marimo-team/skills streamlit-to-marimoInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
For general marimo notebook conventions (cell structure, PEP 723 metadata, output rendering, marimo check, variable naming, etc.), refer to the marimo-notebook skill. This skill focuses specifically on mapping Streamlit concepts to marimo equivalents.
Read the Streamlit app to understand its widgets, layout, and state management.
Create a new marimo notebook following the marimo-notebook skill conventions. Add all dependencies the Streamlit app uses (pandas, plotly, altair, etc.) — but replace streamlit with marimo. You should not overwrite the original file.
Map Streamlit components to marimo equivalents using the reference tables below. Key principles:
.value.Handle conceptual differences in execution model, state, and caching (see below).
Run uvx marimo check on the result and fix any issues.
| Streamlit | marimo | Notes |
|-----------|--------|-------|
| st.slider() | mo.ui.slider() | |
| st.select_slider() | mo.ui.slider(steps=[...]) | Pass discrete values via steps |
| st.text_input() | mo.ui.text() | |
| st.text_area() | mo.ui.text_area() | |
| st.number_input() | mo.ui.number() | |
| st.checkbox() | mo.ui.checkbox() | |
| st.toggle() | mo.ui.switch() | |
| st.radio() | mo.ui.radio() | |
| st.selectbox() | mo.ui.dropdown() | |
| st.multiselect() | mo.ui.multiselect() | |
| st.date_input() | mo.ui.date() | |
| st.time_input() | mo.ui.text() | No dedicated time widget |
| st.file_uploader() | mo.ui.file() | Use .contents() to read bytes |
| st.color_picker() | mo.ui.text(value="#000000") | No dedicated color picker |
| st.button() | mo.ui.button() or mo.ui.run_button() | Use run_button for triggering expensive computations |
| st.download_button() | mo.download() | Returns a download link element |
| st.form() + st.form_submit_button() | mo.ui.form(element) | Wraps any element so its value only updates on submit |
| Streamlit | marimo | Notes |
|-----------|--------|-------|
| st.write() | mo.md() or last expression | |
| st.markdown() | mo.md() | Supports f-strings: mo.md(f"Value: {x.value}") |
| st.latex() | mo.md(r"$...$") | marimo uses KaTeX; see references/latex.md |
| st.code() | mo.md("```python\n...\n```") | |
| st.dataframe() | df (last expression) | DataFrames render as interactive marimo widgets natively; use mo.ui.dataframe(df) only for no-code transformations |
| st.table() | df (last expression) | Use mo.ui.table(df) if you need row selection |
| st.metric() | mo.stat() | |
| st.json() | mo.json() or mo.tree() | mo.tree() for interactive collapsible view |
| st.image() | mo.image() | |
| st.audio() | mo.audio() | |
| st.video() | mo.video() | |
| Streamlit | marimo | Notes |
|-----------|--------|-------|
| st.plotly_chart(fig) | fig (last expression) | Use mo.ui.plotly(fig) for selections |
| st.altair_chart(chart) | chart (last expression) | Use mo.ui.altair_chart(chart) for selections |
| st.pyplot(fig) | fig (last expression) | Use mo.ui.matplotlib(fig) for interactive matplotlib |
| Streamlit | marimo | Notes |
|-----------|--------|-------|
| st.sidebar | mo.sidebar([...]) | Pass a list of elements |
| st.columns() | mo.hstack([...]) | Use widths=[...] for column ratios |
| st.tabs() | mo.ui.tabs({...}) | Dict of {"Tab Name": content} |
| st.expander() | mo.accordion({...}) | Dict of {"Title": content} |
| st.container() | mo.vstack([...]) | |
| st.empty() | mo.output.replace() | |
| st.progress() | mo.status.progress_bar() | |
| st.spinner() | mo.status.spinner() | Context manager |
Streamlit reruns the entire script top-to-bottom on every interaction. Marimo uses a reactive cell DAG — only cells that depend on changed variables re-execute.
st.rerun() — reactivity is automatic.st.stop() — structure cells so downstream cells naturally depend on upstream values.| Streamlit | marimo |
|-----------|--------|
| st.session_state["key"] | Regular Python variables between cells |
| Callback functions (on_change) | Cells referencing widget.value re-run automatically |
| st.query_params | mo.query_params |
| Streamlit | marimo |
|-----------|--------|
| @st.cache_data | @mo.cache | Caches based on function arguments; marimo-aware |
| @st.cache_resource | @mo.persistent_cache | Persists across notebook restarts (serializes to disk) |
@mo.cache is the primary caching decorator — it works like functools.cache but is aware of marimo's reactivity. @mo.persistent_cache goes further by persisting results to disk across sessions, useful for expensive computations like model training.
Marimo offers two approaches for multi-page Streamlit apps:
mo.routes with mo.nav_menu or mo.sidebar to build multiple "pages" (tabs/routes) inside one notebook.marimo run folder/ to serve them as a gallery with navigation.marimo features molab to host marimo apps instead of the streamlit community cloud. You can generate an "open in molab" button via the add-molab-badge skill.
streamlit has a feature for custom components. These are not compatible with marimo. You might be able to generate an equivalent anywidget via the marimo-anywidget skill but discuss this with the user before working on that.
development
Write a marimo notebook in a Python file in the right format.
tools
An opintionated skill to prepare a marimo notebook to make it ready for a scheduled run.
development
Check if a marimo notebook is compatible with WebAssembly (WASM) and report any issues.
tools
Convert a Jupyter notebook (.ipynb) to a marimo notebook (.py).