One component, one list: whatever the user ticks arrives as list[str] in your function.
One component gives your users checkboxes and hands your function a tidy Python list — until you use .select(), which hands you the list from before your click. On purpose.
gr.CheckboxGroup(choices=[...]) renders one checkbox per choice; whatever the user ticks arrives in your function as a list — of the choice values with the default type="value", or of their positions with type="index". Nothing checked is just []. Choices can carry separate labels via tuples: [("ViT-base", "vit-base-patch16-224")].
Multi-select is the natural UI for feature toggles, batch filters, model pickers, and tag input — everywhere a Dropdown is wrong because the user may want more than one. And since the value is always a plain list, the backend code stays boring: no parsing, no splitting on commas, just for x in picks.
import gradio as gr
SECTIONS = ["Summary", "Methodology", "Results", "Limitations"]
def build_report(chosen, audience):
if not chosen:
return "_No sections selected — tick at least one._"
order = [s for s in SECTIONS if s in chosen] # canonical order
body = "\n\n".join(f"## {i}. {s}" for i, s in enumerate(order, 1))
return f"# Report for {audience}\n\n{body}\n\n_{len(order)} of {len(SECTIONS)} sections included._"
with gr.Blocks() as demo:
picks = gr.CheckboxGroup(
choices=SECTIONS,
value=["Summary", "Results"],
label="Sections",
info="Order is fixed; Summary comes first if chosen.",
)
audience = gr.Radio(["engineers", "managers"], value="engineers", label="Audience")
out = gr.Markdown(label="Preview")
picks.change(build_report, [picks, audience], out)
audience.change(build_report, [picks, audience], out)
demo.launch()Console: * Running on local URL: http://127.0.0.1:7860 Rendered UI: a Markdown heading, then a 'Sections' CheckboxGroup with 'Summary' and 'Results' pre-checked (from value=[...]), an 'Audience' radio pair, and a Markdown 'Preview' panel below. Ticking 'Methodology' re-renders the preview to: Report for engineers 1. Summary (drafted 1-word stub) 2. Methodology (drafted 1-word stub) 3. Results (drafted 1-word stub) Verified in a headless browser with gradio 6.28.0. Note the preview is EMPTY until you interact: .change() fires on changes, not on load — pair it with demo.load() if you want the initial render.
The list arrives in choices order, not click order — build_report re-sorts into canonical SECTIONS order on every call.
import gradio as gr
MODELS = ["vit-base-patch16-224", "resnet50", "mobilenet_v3_large"]
def train(inds):
# type="index": inds is a list of INT positions, not the label strings
return f"fn received indices: {inds!r}"
with gr.Blocks() as demo:
picks = gr.CheckboxGroup(choices=MODELS, type="index", label="Baselines to train")
btn = gr.Button("Inspect payload")
out = gr.Textbox(label="What the function received")
btn.click(train, picks, out)
demo.launch()Console:
* Running on local URL: http://127.0.0.1:7860
Rendered UI: three checkbox labels ('vit-base-patch16-224', 'resnet50', 'mobilenet_v3_large') above an 'Inspect payload' button and a 'What the function received' Textbox. Checking the 1st and 3rd boxes, then clicking the button, puts exactly this in the Textbox:
fn received indices: [0, 2]
Verified in a headless browser with gradio 6.28.0.
With type="index" the empty selection preprocesses to [] — a falsy list your function must handle before indexing anything.
import gradio as gr
FILTERS = ["grayscale", "blur", "sharpen", "edge detect"]
def on_toggle(evt: gr.SelectData, current):
action = "ticked" if evt.selected else "unticked"
return f"{action}: {evt.value!r} | full selection: {current}"
with gr.Blocks() as demo:
picks = gr.CheckboxGroup(choices=FILTERS, label="Preprocessing filters")
log = gr.Textbox(label="Event log")
picks.select(on_toggle, [picks], log)
demo.launch()Console: * Running on local URL: http://127.0.0.1:7860 Rendered UI: four checkbox labels and an 'Event log' Textbox underneath. Clicking 'blur' puts this in the log: ticked: 'blur' | full selection: [] Then clicking 'grayscale': ticked: 'grayscale' | full selection: ['blur'] Then unchecking 'blur': unticked: 'blur' | full selection: ['blur', 'grayscale'] Verified in a headless browser with gradio 6.28.0 — and that last line is the gotcha: the current argument is the selection BEFORE this click was applied, because the frontend dispatches the select event on the input event, before the value update lands.
.select() is the only CheckboxGroup event that fires per-toggle; use it for logs and previews, and .change() when you need the post-update list.
| Flag | Meaning |
|---|---|
choices | List of strings, numbers, or (label, value) tuples — the tuple form decouples what users see from what your fn receives. |
value | Pre-checked entries; a single string works too (treated as a one-element list). None starts with nothing checked. |
type="index" | Hand your fn positions (list[int]) instead of values — handy when choices map onto array columns or enum ordinals. |
show_select_all=True | Renders a select/deselect-all checkbox next to the label with an indeterminate state in between; added in Gradio 5.46.0. |
label + info | The group title plus a smaller hint line beneath it — one line of copy beats a paragraph in the docstring. |
type="value" | The default: your fn receives list[str | int | float] of the checked choice values, in the order choices were declared. |
CheckboxGroup shipped with the very first release of the renamed library — the gradio 2.0.0 wheel of May 2021 ships it in gradio/inputs.py, complete with a default=[...] parameter (renamed to value in later versions) and the same type="value" vs "index" duality that survives today.
In the 3.x codebase CheckboxGroup sat next to Checkbox and Radio as three separate input classes; later releases consolidated the plumbing so Radio, Checkbox and CheckboxGroup share the FormComponent preprocessing machinery — which is why all three fire the same change/input/select trio.
The frontend sends the checked values as a JSON array; CheckboxGroup.preprocess() then maps that payload through the choices: with type="value" it returns the list as-is, with type="index" it returns choice_values.index(choice) per entry — I ran preprocess(['b']) on a 2-choice group with type="index" and got [1]. Unknown values raise gr.Error server-side. Postprocess runs the same road in reverse — and it accepts a single string and wraps it: postprocess('x') on a 3-choice group returns ['x']. That symmetry is what lets one component serve as both input and output.