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.$ with gr.Row(): ...- 3.0 — May 16, 2022
- Blocks introduced
- 3.3 — Sep 8, 2022
- Accordion introduced
- 5.14.0 — Jan 30, 2025
- Sidebar introduced
- 13
- Row params on 6.27.0
PyPI upload date of gradio 3.0; Row/Column/Tabs/Group ship with it
PyPI date for 3.3; changelog PR #2208 'Add accordion component'
PyPI date for 5.14.0; changelog PR #10435 'Sidebar Component'
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
- 1Layout is indentation: `with gr.Row():` side-by-side, `with gr.Column():` stacked, nested = tree.
- 2scale splits width relatively (3:1); scale=0 shrink-wraps; min_width is your mobile safety net.
- 3gr.Accordion(open=False) is the standard home for advanced options; toggle it from code with gr.Accordion(open=True).
- 4Return gr.Tabs(selected=...) from a listener to drive wizards — and visible=False to hide whole sections.
- 5gr.Group() removes gaps between related widgets; gr.Sidebar() gives the app-shell look with zero CSS.
