kmail.at
contact

elsewhere

EnglishEN
← learning

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.

2026-10-17 · 5 min read

$ gr.Gallery()
Gradio 3.0
Introduced

2022-05-16 — absent from 2.9.4, released a month earlier

5 → 28 params
Signature

3.0 to 6.30.0, where it has sat since 6.5.0

6
Events

select, upload, change, delete, preview_open, preview_close

webp
Default cache format

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

  1. 1Return a list, always — and make it a list of (media, caption) tuples the moment the caption matters.
  2. 2Set columns and a fixed height; without them a 30-image gallery grows the page until nobody reaches your buttons.
  3. 3Wire .select() only if you mean it: it arrives together with preview_open on the same click.
  4. 4allow_preview=False kills the zoom and the preview_open / preview_close listeners, so check that before blaming your handler.
  5. 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 →

← all learning