gradio · difficulty ◆◆
gradio Gallery — the image grid that hands the clicked tile back
gr.Gallery is a list of images in and one grid out — plus, if you let it, the clicked tile back.
A single click on a Gallery tile fires two listeners, and only the second one turns a contact sheet into a picker.
fully AI-generatedThis text is fully AI-generated and was published without human editorial review.$ gr.Gallery()- Gradio 3.0
- Introduced
- 5 → 28 params
- Signature
- 6
- Events
- webp
- Default cache format
2022-05-16 — absent from 2.9.4, released a month earlier
3.0 to 6.30.0, where it has sat since 6.5.0
select, upload, change, delete, preview_open, preview_close
postprocess writes arrays and PIL images to .webp unless you pass format='png'
What it does
gr.Gallery draws a scrollable grid of images — and, since 6.x, videos — inside your demo, one tile per item, with an optional caption under each tile and a lightbox that opens the tile big on click. You feed it a list: file paths, numpy arrays, PIL images, http URLs, or (media, caption) tuples. It runs the other direction too: interactive=True turns the same grid into an upload target, and .select() hands you back the tile the user clicked, index and caption included.
Why it matters
A classifier that returns one label is a toy; a classifier that returns its top-4 candidates as a contact sheet, scores printed under each tile, is something a visitor understands in two seconds. Gallery is the cheapest way to show a model's actual work. It is also the only image output that can read a choice back, which makes 'pick the frame you meant' about three lines instead of a frontend project.
Example
$ import gradio as gr
import numpy as np
def classify(img):
a = np.asarray(img)
mean = float(a.mean())
return [(np.full((64, 64, 3), max(0, int(mean) - k * 35), dtype=np.uint8),
f"class {k}: {max(0, int(mean) - k * 35)}") for k in range(4)]
demo = gr.Interface(fn=classify, inputs=gr.Image(type="numpy", label="photo"),
outputs=gr.Gallery(label="top-4 scores", columns=4, height=240),
title="tiny classifier")
demo.launch()* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
Interface renders: h1 "tiny classifier", then gr.Image labeled "photo" as a drop zone (button text "Drop Image Here - or - Click to Upload"), then Clear / Submit / Flag / Use via API buttons. After Submit the output block becomes a div.grid-container holding four div.gallery-item tiles; each tile is button.thumbnail-item > img with a div.caption-label underneath reading "class 0: 150", "class 1: 115", "class 2: 80", "class 3: 45" for a flat grey input whose mean pixel is 150. The grid computes four equal columns (139.5px each) and .grid-wrap is pinned to exactly 240px, the height I passed.Verified in gradio 6.18.0 with a real launch plus headless Chromium: a 96x96 grey PNG (mean 150) uploaded through the drop zone, Submit clicked, then the DOM and computed styles read back. The four caption strings are the captions the function returned — captions come from the tuples, not from the label.
$ import gradio as gr
import numpy as np
def frames():
return [(np.full((64, 96, 3), 40 + i * 40, dtype=np.uint8), f"frame {i}") for i in range(6)]
def which(ev: gr.SelectData):
path = ev.value["image"]["path"]
return f"tile {ev.index} -> {ev.value['caption']} ({path.rsplit('/', 1)[-1]})"
with gr.Blocks() as demo:
gal = gr.Gallery(value=frames(), columns=3, rows=2, height=200,
interactive=True, sources=["upload"])
out = gr.Textbox(label="you picked")
gal.select(which, None, out)
demo.launch()* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
Six tiles in 3 columns x 2 rows, each with its caption under it, and — because interactive=True — a delete button on every tile (6 of them; the same gallery without interactive=True renders 0). Clicking tile index 2 posts two jobs to /gradio_api/queue/join and the handler receives SelectData: index=2, selected=True, value={'image': {'path': '.../gradio/86bfdb4a.../image.webp', 'url': 'http://127.0.0.1:7860/gradio_api/file=.../image.webp', 'size': None, 'orig_name': None, 'mime_type': None, 'is_stream': False}, 'caption': 'frame 2'}. The Textbox then reads "tile 2 -> frame 2 (image.webp)".Verified in headless Chromium against gradio 6.18.0 with the select handler logging server-side. Uploading a real file named my_photo.png instead gives orig_name='my_photo.png', size=259, mime_type='image/png' — generated tiles keep no original filename.
$ import gradio as gr
import numpy as np
with gr.Blocks() as demo:
gal = gr.Gallery(value=[(np.full((64, 96, 3), 60 + i * 45, dtype=np.uint8), f"shot {i}")
for i in range(5)],
columns=5, height=180, selected_index=2)
status = gr.Textbox(label="lightbox")
gal.preview_open(lambda: "preview opened", None, status)
gal.preview_close(lambda: "preview closed", None, status)
demo.launch()* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
Five tiles across one row. Because of selected_index=2, tile number 3 already carries the class `selected` on first paint and the span.preview lightbox is open on load (1 preview node in the DOM; the same gallery without the parameter has 0), with a strip of thumbnail-small navigator tiles beside the big image. Escape fires preview_close and the Textbox flips to "preview closed".Verified in headless Chromium: with allow_preview=False on the same gallery, clicking a tile opens nothing and neither preview_open nor preview_close ever fires — the Textbox stays empty.
$ import time
import numpy as np
import gradio as gr
def sheet():
step = int(time.time()) % 6
return [(np.full((64, 96, 3), 30 + step * 35 + i * 5, dtype=np.uint8), f"frame {i}")
for i in range(6)]
with gr.Blocks() as demo:
gr.Gallery(value=sheet, every=1.5, columns=3, height=200, label="live sheet")
demo.launch()* Running on local URL: http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
A self-refreshing contact sheet: every 1.5s the queue re-invokes sheet() and repaints the grid — 6 calls logged during roughly 15 seconds of headless browsing, with the first tile's img src moving from .../372e90072f9c.../image.webp to .../0dde3de51baa.../image.webp as the pixels change. When the returned pixels are identical, the src stays put: the cache directory is a content hash, so identical tiles dedupe.Two practical consequences I hit while measuring: an every= gallery keeps the browser polling, so a Playwright page.goto(..., wait_until='networkidle') never returns — use 'load'; and the callable is re-invoked with no arguments on every tick, so any counter you keep in a closure keeps growing between runs.
Common flags
- value
- A list: paths, numpy arrays, PIL images, http URLs or (media, caption) tuples. A bare string raises ValueError.
- columns / rows / height
- columns defaults to 2 and rows to None; pin height to get a scrolling grid instead of a page that grows with every tile.
- object_fit
- Computed 'cover' by default — tiles are cropped to fill. 'contain' letterboxes tall images instead of cutting them off.
- allow_preview / preview
- allow_preview=True (default) gives the click-to-zoom lightbox; False removes it and silences preview_open/preview_close with it.
- selected_index
- Pre-select a tile on load: it gets the CSS class `selected` and the lightbox opens on it.
- interactive / sources
- interactive=True turns the grid into an upload input; sources picks from upload / webcam / clipboard ('sources' arrived in 6.5.0).
- type
- 'filepath' (default), 'numpy' or 'pil' — but the handler always receives (media, caption) tuples, never bare images.
- format
- Arrays and PIL images you return are cached as .webp unless you ask for jpeg/png.
History
Born with Blocks, output-only
There is no Gallery class anywhere in gradio 2.9.4 (2022-04-15) — the component shipped a month later inside Gradio 3.0 (2022-05-16), in the monolithic gradio/components.py, with five parameters: value, label, show_label, visible, elem_id. Its docstring then said it outright: 'Preprocessing: this component does *not* accept input.' For three years the gallery was a shelf you put pictures on, and nothing more.
4.0 gave it a select event
The 4.0 rewrite (2023-10-31) moved it into its own gradio/components/gallery.py and grew the signature from 5 to 20 parameters: columns, rows, height, allow_preview, preview, selected_index, object_fit — and the component's first EVENTS entry, select. upload and change followed in 5.0 (2024-10-09), delete plus preview_open/preview_close in 6.0 (2025-11-21), and the input-side sources list in 6.5 (2026-01-28). It has held at 28 parameters since.
Under the hood
What postprocess and preprocess actually do
postprocess takes your list and normalises every item: numpy arrays go through save_img_array_to_cache, PIL images through save_pil_to_cache (both with `format`, hence the .webp files), paths and Paths are kept as-is, an http URL is passed through untouched, and a .svg is inlined as a data: URL instead of a file. The saves run in a ThreadPoolExecutor, so sixty tiles are written in parallel, and the result is a GalleryData(root=[GalleryImage(ImageData(...))]) — or GalleryVideo(FileData(...)) for anything whose mimetype says video. preprocess, the input direction, always returns list[tuple[media, caption]]: with no captions you get (media, None), so a handler written for the captioned case never sees a different shape, and `type` only decides whether the media object is a server-side path or a decoded numpy/PIL image.
Fun facts
Pros & cons
pros
- + Captions per tile with no extra components: return (image, 'class 3: 80') and the text renders under the tile.
- + Output and input in one component — the same grid that shows the results can accept an upload and report the clicked tile back.
- + The lightbox, the download / share / fullscreen buttons and the parallel save are all free; you never write CSS or resize anything.
cons
- − Every array or PIL image you return is written to disk as a file per tile, webp by default — a 200-tile gallery is 200 cache writes per call.
- − There is no separate caption list: the only way to get text under a tile is a tuple, so a bare list of paths can never be captioned.
- − The select payload is a server-side cache path, and for generated tiles it carries no original filename at all — only an uploaded file keeps its orig_name.
Takeaways
- 1Return a list, always — and make it a list of (media, caption) tuples the moment the caption matters.
- 2Set columns and a fixed height; without them a 30-image gallery grows the page until nobody reaches your buttons.
- 3Wire .select() only if you mean it: it arrives together with preview_open on the same click.
- 4allow_preview=False kills the zoom and the preview_open / preview_close listeners, so check that before blaming your handler.
- 5Pass format='png' when the tiles are diagrams or plots; webp is smaller, and for photos you lose nothing.
Related commands
archive entry — written and published by a scheduled job, never editorially reviewed. Kept for reference: out of the site’s search index, and no ads run here. the reviewed library →
