Place notebook results in a view
A view places notebook results by name: a cell, an output, or a value. Each format writes them in its own syntax:
| Result | Page | Quarto | Typst | LaTeX |
|---|---|---|---|---|
| Cell | <marimo-cell name="filters"> | {{< marimo cell="filters" >}} | #marimo_cell("filters") | \marimocell{filters} |
| Output | <marimo-output value="chart"> | {{< marimo output="chart" >}} | #marimo_output("chart") | \marimographics{chart} |
| Value | <span mo-value="totals.rooms"> | {{< marimo value="totals.rooms" >}} | #marimo_value("totals.rooms") | \marimovalue{totals.rooms} |
On a page, each result goes in a host, an element that Studio fills with the result while the page is open. The built-in starters write the page as plain HTML, React, Svelte, Reveal.js, or Notebook Kit source, and a view provider can build it from any other framework. Write the hosts inside the page's app shell, the #app-shell element, in the markup of the view's framework. Quarto builds a page too, and Studio supplies #app-shell for it. Typst and LaTeX compile a PDF, a render that holds no controls, so their source places images and values.
How Studio works shows what each host receives while the page is open, and Select results with Lens traces any placed result back to its cell.
Name results in the notebook
A cell is named by its function name. Write def sport_control(...) in the notebook file, or rename the cell in the editor.
An output or a value is named by a variable that a cell defines, and a value selector can step into it:
totals.rooms
rows[0]
results["overview"]Give an unnamed cell a cell alias when renaming its function is not an option. List the cells, then bind the alias to one by the zero-based index, name, or ref that the listing prints:
uvx marimo-studio notebook inspect --target analysis.py
uvx marimo-studio notebook bind summary --target analysis.py --cell 1The alias belongs to the notebook, so every view can use it.
Place a cell
@app.cell
def sport_control(athletes, mo):
sport = mo.ui.dropdown(
options=athletes["sport"].unique().sort().to_list(),
label="Sport",
)
sport
return (sport,)<marimo-cell name="sport_control"></marimo-cell>A page shows the cell as the notebook does, with its native controls, logs, and errors. Changing the dropdown reruns the cells that depend on sport, and every result on the page updates.
In a document, marimo_cell() and \marimocell place the image a cell shows, such as the PNG of a matplotlib figure. A cell that shows a control, text, or a table places nothing there, so read its value instead: #marimo_value("sport.value").
Place an output
marimo-output renders one Python object with marimo's own renderer, even an object that no cell displays:
@app.cell
def rooms(mo):
room_table = mo.ui.table([{"room": "A", "in_use": 12}, {"room": "B", "in_use": 30}])
return (room_table,)<marimo-output value="room_table"></marimo-output>In a document, an output must have an image form. marimo_output() and \marimographics draw a matplotlib figure or an Altair chart as a vector PDF at the width the document places it at, so its labels print at the point size the notebook set:
#import "marimo.typ": marimo_output
#figure(marimo_output("occupancy_chart", width: 100%), caption: [Rooms in use.])\begin{figure}
\marimographics[width=\linewidth]{occupancy_chart}
\caption{Rooms in use.}
\end{figure}Place a value
On a page, mo-value writes a number, text, or boolean into the element:
@app.cell
def occupancy():
totals = {"day": "2026-10-20", "rooms": 42, "visits": 1250}
return (totals,)<p>Rooms in use: <strong mo-value="totals.rooms"></strong></p>Page code can also read the value and draw it however it likes. The host exposes the current value as host.marimoValue and dispatches marimo-value-updated whenever it changes:
<span id="totals-data" hidden mo-value="totals"></span>
<output id="visits"></output>
<script type="module">
const host = document.querySelector("#totals-data");
const output = document.querySelector("#visits");
const render = (value) => {
output.value = value.visits.toLocaleString();
};
host.addEventListener("marimo-value-updated", (event) => {
render(event.detail.value);
});
if (host.marimoValue !== undefined) {
render(host.marimoValue);
}
</script>Register the listener before reading marimoValue. undefined means the value has not arrived yet, and JSON null is a real value. Listen for marimo-value-error to show a local error state. React starters wrap these events in a useMarimoValue hook, and Svelte starters in a use:observeMarimoValue action.
A document reads a value while it renders:
#import "marimo.typ": marimo_value
Rooms in use: #marimo_value("totals.rooms", default: 0)Rooms in use on \marimodate{totals.day}: \marimonum{totals.rooms}.Tables in page code
An eager dataframe reaches page code as a Flechette Table. Studio sends it as Arrow IPC, a columnar binary format, and decodes it before it updates the host:
const chartRows = table.select(["region", "revenue"]).toArray();Treat the table as read-only. Keep it columnar while you select fields or pass it to a column-oriented library, and call toArray() where a consumer needs row objects. Materialize lazy or remote queries in the notebook before a view reads them.
Renders read JSON
Typst and LaTeX renders read each value in its JSON form:
| Notebook value | Render reads |
|---|---|
| pandas, Polars, or PyArrow table | A list of row objects keyed by column name |
date, datetime, time | ISO 8601 text, such as 2015-02-04T09:41:00 |
NumPy number, Decimal, or Enum member | Its number or value |
| NaN, or a missing table cell | null |
A step through None, such as peak.label | null, while peak is None |
| Dictionary, list, text, number, boolean | The same value |
The Typst or LaTeX source formats what it reads. LaTeX has \marimonum, \marimodate, and \marimorows, which repeats a table row for each row of a dataframe. Typst uses its own functions. A null value falls back to the source's default.
A value whose JSON form holds more than 100,000 values or 1,000,000 bytes fails with value-too-large. Filter or aggregate large tables in the notebook first. Limits lists every bound.
Choose results at runtime
React and Svelte providers find the targets a view uses while they build it. Literal names and finite arrays work:
export const Results = () => (
<section>
{["summary", "details"].map((name) => (
<marimo-cell key={name} name={name} />
))}
</section>
);When page state can pick any notebook target, declare that on the host:
<marimo-cell name={selectedName} data-marimo-allow="*" />data-marimo-allow="*" lets that host request any target of the same kind. Put it on the narrowest host that needs it. A computed name without it fails provider inspection. A Prepared export needs finite targets and rejects wildcard hosts, so export a view that picks targets at runtime with --runtime wasm.
Check the names
Check that every name a view uses exists in the notebook, without running it:
uvx marimo-studio validate dashboard --target analysis.pyAdd --level runtime to run the notebook and check each result the view reads. Runtime validation runs every cell, including its file, network, and database access, so use it with notebooks you trust.
Notebook result projections defines host attributes, events, loading states, and limits.