streamlit-to-marimo
Converting Streamlit Apps to Marimo
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.
Steps
-
Read the Streamlit app to understand its widgets, layout, and state management.
-
Create a new marimo notebook following the
marimo-notebookskill conventions. Add all dependencies the Streamlit app uses (pandas, plotly, altair, etc.) — but replacestreamlitwithmarimo. You should not overwrite the original file. -
Map Streamlit components to marimo equivalents using the reference tables below. Key principles:
- UI elements are assigned to variables and their current value is accessed via
.value. - Cells that reference a UI element automatically re-run when the user interacts with it — no callbacks needed.
- UI elements are assigned to variables and their current value is accessed via
-
Handle conceptual differences in execution model, state, and caching (see below).
-
Run
uvx marimo checkon the result and fix any issues.
Widget Mapping Reference
Input Widgets
| 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 |
Display Elements
| 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() |
Charts
| 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 |
Layout
| 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 |
Key Conceptual Differences
Execution Model
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.
- No need for
st.rerun()— reactivity is automatic. - No need for
st.stop()— structure cells so downstream cells naturally depend on upstream values.
State Management
| 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 |
Caching
| Streamlit | marimo |
|---|---|
@st.cache_data |
@mo.cache |
@st.cache_resource |
@mo.persistent_cache |
@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.
Multi-Page Apps
Marimo offers two approaches for multi-page Streamlit apps:
- Single notebook with routing: Use
mo.routeswithmo.nav_menuormo.sidebarto build multiple "pages" (tabs/routes) inside one notebook. - Multiple notebooks as a gallery: Run a folder of notebooks with
marimo run folder/to serve them as a gallery with navigation.
Deploying
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.
Custom components
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.