kmail.at
contact

elsewhere

EnglishEN
← learning

gradio · difficulty ◆◆

gradio Blocks layouts — Row, Column, Tabs, Accordion, Group and Sidebar

Layout in Gradio is indentation: wrap components in with gr.Row/Column/Tabs blocks and the tree is your UI.

Gradio's layout engine doesn't read CSS from you — it reads your indentation.

AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.

2026-09-15 · 5 min read

$ with gr.Row(): ...
3.0 — May 16, 2022
Blocks introduced

PyPI upload date of gradio 3.0; Row/Column/Tabs/Group ship with it

3.3 — Sep 8, 2022
Accordion introduced

PyPI date for 3.3; changelog PR #2208 'Add accordion component'

5.14.0 — Jan 30, 2025
Sidebar introduced

PyPI date for 5.14.0; changelog PR #10435 'Sidebar Component'

13
Row params on 6.27.0

counted from gr.Row.__init__ via inspect (excluding self)

What it does

Inside gr.Blocks, layout containers are Python context managers you nest with `with`: gr.Row lays children side by side, gr.Column stacks them vertically (both are flexboxes with a scale knob — Row's scale is relative height, Column's is relative width, and Column also takes min_width, default 320), gr.Tabs + gr.Tab give click-to-switch panels, gr.Accordion is a collapsible section (open= controls the default, which is True), gr.Group glues related components into one visual box, and gr.Sidebar (5.14.0) adds a fixed collapsible panel on the left (width=320, position='left', open=True). Nesting them builds a tree: Rows contain Columns, Tabs contain Tab panels, an Accordion can live inside a Column.

Why it matters

Components alone are a form; layout is what makes a demo feel like a product. The same classifier becomes a settings sidebar, a main results panel and an 'Advanced options' accordion that hides complexity from casual users — without one line of HTML or CSS. Two parameters do most of the work: scale (relative flex weight, 0 = shrink to content) and visible=False (a container and everything inside it disappears, which is the standard trick for multi-step wizards). Every rendered demo on Hugging Face Spaces is this same with-block tree.

Example

$ # Row + Column + scale: a two-panel workspace
import gradio as gr

with gr.Blocks(title="Echo Studio") as demo:
    with gr.Row():
        t = gr.Textbox(label="prompt", scale=3)
        b = gr.Button("Run", variant="primary", scale=1)
    o = gr.Textbox(label="result")

    @b.click(inputs=t, outputs=o)
    def run(x):
        return (x or "").upper()

demo.launch(prevent_thread_lock=True)
* Running on local URL:  http://127.0.0.1:7860

Rendered UI (verified via /config on 6.27.0): component tree ['row', 'textbox', 'button', 'form', 'textbox', 'form'] — a Row holding the Textbox "prompt" (scale=3, wider) and the primary Button "Run" (scale=1), with the Textbox "result" full-width below. Calling the API end-to-end: POST /gradio_api/call/run with ["hello blocks"] streams back data: ["HELLO BLOCKS"].

Verified by launching on 6.27.0 and reading /config plus calling the event API. scale=3 vs scale=1 splits the row's width 3:1; variant="primary" is what paints the button orange.

$ # Tabs + Accordion: hide the expert stuff
data = {"2026-09-13": [12, 18, 9], "2026-09-14": [22, 31, 17], "2026-09-15": [15, 24, 11]}

with gr.Blocks(title="Latency Explorer") as demo:
    gr.Markdown("## API latency explorer")
    with gr.Tabs():
        with gr.Tab("Today"):
            with gr.Row():
                day = gr.Dropdown(choices=list(data), value="2026-09-15", label="day")
                stat = gr.Radio(["mean", "max"], value="mean", label="stat")
        with gr.Tab("Help"):
            gr.Markdown("pick a day, then read the summary panel")
    with gr.Row():
        with gr.Column(scale=2):
            table = gr.Dataframe(label="samples")
        with gr.Column(scale=1):
            big = gr.Number(label="summary")
            with gr.Accordion("raw payload", open=False):
                raw = gr.JSON(label="payload")

    @day.change(inputs=[day, stat], outputs=[table, big, raw])
    def summarize(d, s):
        vals = data[d]
        return ([[i + 1, v] for i, v in enumerate(vals)],
                sum(vals) / len(vals) if s == "mean" else max(vals),
                {"day": d, "values": vals, "stat": s})

demo.launch(prevent_thread_lock=True)
* Running on local URL:  http://127.0.0.1:7860

Rendered UI (verified via /config on 6.27.0): types ['markdown', 'tabs', 'tabitem', 'row', 'dropdown', 'radio', 'form', 'tabitem', 'markdown', 'row', 'column', 'dataframe', 'column', 'number', 'accordion', 'json', 'form'] — tabitem labels ['Today', 'Help'] confirmed in the config. Handler check: summarize("2026-09-14", "mean") -> [[[1, 22], [2, 31], [3, 17]], 23.33..., {"day": "2026-09-14", "values": [22, 31, 17], "stat": "mean"}].

Verified by executing and inspecting /config. The Accordion ships closed (open=False) so the JSON stays out of sight until clicked; the two Columns split 2:1 via scale.

$ # Switching tabs from Python: return gr.Tabs(selected=...)
with gr.Blocks() as demo:
    with gr.Tabs() as tabs:
        with gr.Tab("Alpha"):
            a = gr.Textbox(label="a")
        with gr.Tab("Beta"):
            b = gr.Textbox(label="b")
    btn = gr.Button("jump to Beta")

    @btn.click(outputs=tabs)
    def jump():
        return gr.Tabs(selected="Beta")

demo.launch(prevent_thread_lock=True)
* Running on local URL:  http://127.0.0.1:7860

Verified through the event API on 6.27.0: POST /gradio_api/call/jump -> event streams 'event: complete' with data: [{"selected": "Beta", "__type__": "update"}] — the UI switches to the Beta panel. Same pattern for Accordion: a handler returning gr.Accordion(open=True) streams {"open": true, "__type__": "update"} and pops the section open.

Verified by executing and calling the endpoint. Returning gr.Tabs(selected=...) from a listener is the documented way to drive navigation in code — think wizard: validate step 1, then auto-advance to step 2.

$ # Sidebar + Group: the app shell look
with gr.Blocks(fill_height=True) as demo:
    with gr.Sidebar():
        gr.Textbox(label="filter")
        gr.Checkbox("only new")
    with gr.Group():
        gr.Markdown("### Settings")
        gr.Slider(0, 2, value=0.7, label="temperature")
    gr.Textbox(label="content")

demo.launch(prevent_thread_lock=True)
* Running on local URL:  http://127.0.0.1:7860

Rendered UI (verified via /config on 6.27.0): types ['sidebar', 'textbox', 'checkbox', 'form', 'group', 'markdown', 'slider', 'form', 'textbox', 'form'] — the Sidebar renders as a fixed left panel (default width=320, position="left") holding the filter controls; the Group draws one bordered box around its Markdown + Slider. Sidebar has its own expand/collapse events.

Verified by executing and inspecting /config. Sidebar arrived in 5.14.0 (PR #10435, Jan 30, 2025 per PyPI); on mobile it collapses over the content instead of pushing it.

Common flags

with gr.Row()
Horizontal flexbox; children sit side by side. equal_height defaults to False — set it True to stretch every child to the tallest; variant="panel" adds a card-style box around the row.
scale (Row/Column)
Relative flex weight. scale=3 vs scale=1 gives a 3:1 split; scale=0 makes a child shrink-wrap its content.
min_width (Column/Row children)
Pixel floor for a flex child — stops scale from squeezing a dropdown into unreadability on narrow screens.
gr.Tabs / gr.Tab(label)
Click-to-switch panels; Tab(label, id=...) and tabs.select let you react, gr.Tabs(selected=...) returned from a listener switches panels programmatically.
gr.Accordion(label, open=False)
Collapsible section; open defaults to True, so pass open=False when advanced options should stay out of the screenshot. Toggle it from code with gr.Accordion(open=...).
gr.Group()
Glues adjacent components into one visual unit (shared border, no gaps) — for a value+unit pair or a labeled settings cluster.
visible=False
On any container: hides it and every descendant. The cheapest multi-step-wizard primitive; flip it back from any event listener.
gr.Sidebar()
Collapsible left panel (width=320 default, position='left'|'right'); pair with gr.Blocks(fill_height=True) so the main column fills the viewport.
variant (Row/Column)
Row and Column both take variant: 'default' (no background), 'panel' (gray card with rounded corners) or 'compact' (rounded corners, no internal gap).

History

May 2022: Blocks made layout Python

Gradio 3.0 (PyPI 2022-05-16) replaced the fixed two-column Interface with gr.Blocks, where Row, Column, Tabs, Group and Tab are context managers you nest — the changelog's own 3.0 entries already patch tab internals (PR #1200 wraps tab content in a column). The pitch: your indentation is your layout, no HTML. Accordion followed fast, in Gradio 3.3 (2022-09-08, PR #2208 by aliabid94).

Jan 2025 and Nov 2025: the shell grows up

gr.Sidebar landed in 5.14.0 (PR #10435, dawoodkhan82) — a fixed collapsible panel that made single-page app shells possible in pure Gradio. Then Gradio 6 (6.0.0, PyPI 2025-11-21) moved app-wide look-and-feel (theme, css, js) from the Blocks constructor into launch(), leaving the layout constructors to do exactly one thing: structure.

Under the hood

Under the hood: /config is the layout

Every container is itself a component in the /config JSON the server emits at launch — row, column, tabs, tabitem, accordion, group, sidebar all appear in the same components array as your widgets, in tree order (my probes read the rendered tree straight from that payload). Layout children never round-trip values; they carry structure and props (scale, visible, open, selected). When a listener returns gr.Tabs(selected='Beta') the backend streams {"selected": "Beta", "__type__": "update"} over SSE and the frontend patches the tabs component's props — same update protocol as any widget, just aimed at a container.

Fun facts

Pros & cons

pros

  • + Whole layout is Python — indentation is the UI tree, zero HTML/CSS for structure
  • + Progressive disclosure built in: Tabs and Accordion hide complexity without extra state management
  • + Containers are components too, so visible=/scale= work uniformly everywhere

cons

  • − Flexbox-only: no absolute positioning, no grid spanning — pixel-perfect designs still need css= or custom components
  • − Deep nesting gets hard to read fast (Rows in Columns in Tabs in Accordions) — extract helper functions or you'll lose track of the with-levels
  • − Sidebar is desktop-first; on phones it overlays content, which can cover a form mid-fill

Takeaways

  1. 1Layout is indentation: `with gr.Row():` side-by-side, `with gr.Column():` stacked, nested = tree.
  2. 2scale splits width relatively (3:1); scale=0 shrink-wraps; min_width is your mobile safety net.
  3. 3gr.Accordion(open=False) is the standard home for advanced options; toggle it from code with gr.Accordion(open=True).
  4. 4Return gr.Tabs(selected=...) from a listener to drive wizards — and visible=False to hide whole sections.
  5. 5gr.Group() removes gaps between related widgets; gr.Sidebar() gives the app-shell look with zero CSS.

Related commands