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.

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 scale/min_width knobs), gr.Tabs + gr.Tab give click-to-switch panels, gr.Accordion is a collapsible section (open= controls the default), gr.Group glues related components into one visual box, and gr.Sidebar (5.14.0) adds a fixed collapsible panel on the left. 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.

Examples

# 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.

Flags

FlagMeaning
with gr.Row()Horizontal flexbox; children sit side by side. equal_height=True (the default) stretches them to match, 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, closed by default so advanced options stay out of the screenshot; toggle 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=FalseOn 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.

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: /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

Takeaways