Skip to content

Design a view

Choose the notebook behavior an audience needs, then build the page around it. Keep calculations and interactive components in the notebook. Put layout, wording, navigation, and visual design in the view.

Arrange the workspace

Studio keeps three surfaces alive while you work:

  • Notebook runs the native Marimo editor.
  • Source edits index.html and app.css.
  • Preview renders the selected view against the notebook kernel.

The default layout places the notebook and Preview side by side at equal size. Open Pane in any pane header to add a hidden surface on any side, swap two visible panes, or close a pane. Each divider moves independently. Use Layout to equalize split sizes or restore the default. Focus temporarily fills the workspace with one pane. Press Escape to restore the saved arrangement.

Narrow windows show one surface at a time while keeping the full layout and its ratios. Studio stores layout and source-tab choices in the browser for each view.

Choose what to place in the page

NeedUse
A complete control, plot, table, download, or anywidget<marimo-cell>
A date, count, label, or other JSON-compatible valuemo-value
A large detail region that should appear on demandAn HTMX cell route
New formatting or derived presentation dataA small notebook cell

Start with existing notebook outputs. Add a small presentation value cell when the notebook does not expose the required value.

Place a complete notebook output

Use <marimo-cell> with a native cell name or an alias created by bind:

html
<section aria-labelledby="revenue-title">
  <h2 id="revenue-title">Revenue</h2>
  <marimo-cell class="chart-cell" name="revenue_chart"></marimo-cell>
</section>

Marimo renders the cell through the same output plugins and model clients used by its native interface. Controls, tables, plots, downloads, and anywidgets remain connected to the current Python session.

Each cell name can appear once in a view. Reuse the same name in another view when both audiences need that output.

Place a Python value in page text

Use mo-value when the page needs one value rather than a complete cell:

html
<dl>
  <div>
    <dt>Last updated</dt>
    <dd><time mo-value="report.updated_at"></time></dd>
  </div>
  <div>
    <dt>Selected records</dt>
    <dd><strong mo-value="selection.count"></strong></dd>
  </div>
</dl>

A selector starts with one notebook variable and can continue through:

  • .name for a mapping key or Python attribute
  • [0] for a non-negative item index
  • ["key.with.dots"] for a JSON string item key
html
<span mo-value="series[0].label"></span>
<span mo-value='metadata["key.with.dots"]'></span>

The root variable must have one defining cell. The selected leaf renders strings, numbers, and booleans as text. Objects and arrays render as compact JSON. A null value renders as empty text.

Keep arithmetic, formatting, slicing, and function calls in Python:

python
@app.cell
def _(df):
    report = {
        "updated_at": f"{df['Date'].max():%d %b %Y}",
        "rows": len(df),
    }
    return (report,)

The variable can be a dictionary, a list, a scalar, or another value with a JSON-compatible selected leaf. Several small context cells let unrelated reactive branches update independently.

Structure the view as an ordinary web page

Each index.html is a complete HTML document with one #app-shell:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Revenue dashboard</title>
    <link
      rel="stylesheet"
      href="./_marimo-studio/views/dashboard/static/app.css"
    >
  </head>
  <body>
    <main id="app-shell">
      <h1>Revenue dashboard</h1>
      <marimo-cell name="revenue_chart"></marimo-cell>
    </main>
  </body>
</html>

Place every <marimo-cell> and mo-value inside #app-shell. Saving HTML replaces this shell, while CSS reloads independently. Your preview keeps its current kernel and widget models.

Files in the view folder use the view's scoped static route:

html
<img
  src="./_marimo-studio/views/dashboard/static/logo.svg"
  alt="Acme logo"
>

Relative URLs continue to work when Marimo serves beneath a configured base path.

Reveal detail on demand

HTMX is available as window.htmx. A view can request a fresh cell element when the audience asks for more detail:

html
<button
  type="button"
  hx-get="./_marimo-studio/views/dashboard/cells/detail_table"
  hx-target="#details"
  hx-swap="innerHTML"
>
  Show details
</button>
<section id="details" aria-live="polite"></section>

The response inserts <marimo-cell name="detail_table"> into #details. The cell connects to the current Marimo session, so the page keeps its controls, widget models, and reactive state.

Use this pattern for secondary tables, diagnostics, and other deferred regions.

Keep the layout steady while cells load

Each cell shows a skeleton before its first output arrives. Reserve a realistic height for charts, tables, and other substantial regions:

css
marimo-cell[name="revenue_chart"] {
  --marimo-cell-skeleton-height: 28rem;
  --marimo-cell-skeleton-color: rgb(20 24 32 / 9%);
  --marimo-cell-skeleton-radius: 0.35rem;
}

time[mo-value] {
  --marimo-value-skeleton-width: 12ch;
}

The cell keeps that minimum height while its output plugin mounts. After a successful render, the browser remembers the measured height for the same view and viewport class.

Use data-skeleton="none" when an empty first-load region is intentional:

html
<marimo-cell name="status" data-skeleton="none"></marimo-cell>

Match notebook output to the page

Set the page color scheme so Marimo controls choose a matching theme:

css
:root {
  color-scheme: light;
}

Mounted output inherits the surrounding font and color. Use Studio's CSS properties to align its surfaces and accents:

css
.chart-cell {
  --marimo-cell-font: Inter, ui-sans-serif, system-ui, sans-serif;
  --marimo-cell-background: transparent;
  --marimo-cell-foreground: #202124;
  --marimo-cell-surface: #fff;
  --marimo-cell-muted: #f3f3f1;
  --marimo-cell-border-color: #d8d7d2;
  --marimo-cell-accent: #315f82;
  --marimo-cell-radius: 0.25rem;
  --marimo-cell-padding: 0;
}

Application CSS owns the layout, spacing, borders, and responsive behavior around the output. See Loading and theming for the complete property list.

Build a second experience from the same notebook

Add another view when an audience needs different results or page structure. Open the current view menu and select New view. Studio opens the notebook on the left, the new HTML on the upper right, and its live preview below. The starter HTML places every notebook cell in source order, and app.css provides a small responsive base.

The same operation is available from the command line:

console
uvx marimo-studio view add operations analysis.py

The operations view receives its own HTML, CSS, and static files. It reuses the notebook's cells and aliases.

Remove a view from the same menu. Studio confirms the exact view before it deletes that view's HTML, CSS, and static files. Removing the default view promotes the first remaining view. A notebook always keeps at least one view.

Repair a projection after notebook changes

Saving the notebook refreshes each view against the current cell graph. When a view references an undefined cell or variable, the rest of the page stays active and the affected projection shows a compact issue. Studio shows repair details while you edit. Shared run views keep the message audience-safe.

Restore the notebook definition, update index.html, or rebind an anonymous cell. Studio clears the issue after the next successful save:

console
uvx marimo-studio inspect analysis.py --display
uvx marimo-studio bind summary analysis.py --cell 4 --overwrite
uvx marimo-studio check analysis.py --view operations

Agents can request the same findings as JSON and JSON Lines:

console
uvx marimo-studio check analysis.py \
  --view operations \
  --format json \
  --diagnostics jsonl

Wait for a settled view in browser automation

Cell and value elements expose their current state through data-state. Browser tests and agents can wait until the current view settles:

js
await window.marimoStudio.ready();
const diagnostics = window.marimoStudio.diagnostics();

The promise resolves when every current cell and value has rendered content, retained content while updating, or reached a terminal error. It also waits for an in-flight HTML, CSS, or notebook refresh. diagnostics() returns projection findings and browser lifecycle failures. See Browser readiness for the complete contract.

Run a runtime check before sharing the view:

console
uvx marimo-studio check analysis.py --view operations --runtime