Select#

A single-selection dropdown field with optional search filtering.

Select — light theme Select — dark theme

Usage#

Basic#

bs.Select(["Red", "Green", "Blue"])
bs.Select(["Red", "Green", "Blue"], value="Green")

Option values#

An option’s displayed label can differ from its stored value. Each option is a plain string, a (text, value) tuple, or a {"text": ..., "value": ...} dict. value=, .value, and the change event all work in value-space — the value, not the label. The label currently shown is available as .text.

theme = bs.Select(
    [("Light theme", "light"), ("Dark theme", "dark"), {"text": "Follow system", "value": "auto"}],
    value="dark",
)
theme.value            # -> "dark"          (the value)
theme.text             # -> "Dark theme"    (the displayed label)
theme.value = "auto"   # selects "Follow system"
theme.on_change(lambda e: apply_theme(e.value))

theme.options          # -> [{"text": "Light theme", "value": "light"}, ...]

Setting .value to something that is not one of the options raises ValueError (unless allow_custom_values=True, where a typed value is kept as its own text).

Carrying extra data (the data bag)#

The dict form is a data bag — alongside the recognized keys (text, value, and the per-option icon/disabled), any other key you add rides along as carried data. Read the whole selected record, indexed by key, via .selection:

country = bs.Select(options=[
    {"text": "Canada", "value": "CA", "phone": "+1"},
    {"text": "Japan",  "value": "JP", "phone": "+81"},
], value="JP")

country.value             # -> "JP"
country.selection         # -> {"text": "Japan", "value": "JP", "phone": "+81"}
country.selection["phone"]  # -> "+81"

country.on_change(lambda e: dial(country.selection["phone"]))

.selection is the selected option’s full dict (the same shape you’d get from a ListView row), or None when nothing is selected. Unrecognized keys are accepted, not validated — the dict route is opt-in, so a mistyped key rides along silently rather than raising.

Two of the recognized keys change how an option is presented: icon renders a glyph beside the option’s label in the popup, and disabled greys the row out and makes it non-selectable (keyboard navigation and search auto-select skip it too). Per-option disabled is independent of the widget-level disabled= that locks the whole control:

bs.Select(options=[
    {"text": "Free",       "value": "free",  "icon": "gift"},
    {"text": "Pro",        "value": "pro",   "icon": "star"},
    {"text": "Enterprise", "value": "ent",   "icon": "buildings", "disabled": True},
], value="free")

Setting a disabled option’s value programmatically still works — disabled only blocks user selection.

Grouping#

Pass group_by="field" to cluster the popup under section headers, where field is any key your options carry — often a category that already lives in your data. Grouping is purely presentational: value, .selection, and .options are unaffected, and the grouping field rides along in the data bag.

bs.Select(
    options=[
        {"text": "Apple",    "value": "apple",    "category": "Fruit"},
        {"text": "Banana",   "value": "banana",   "category": "Fruit"},
        {"text": "Carrot",   "value": "carrot",   "category": "Vegetable"},
        {"text": "Basil",    "value": "basil",    "category": "Herb"},
    ],
    group_by="category",
    label="Ingredient",
)

Groups appear in first-appearance order; an option that lacks the field renders without a header. Header text is shown verbatim — it is never re-cased or otherwise transformed, so .selection still returns the original value ({..., "category": "Fruit"}).

Select grouping — light theme Select grouping — dark theme

Limiting the popup height#

The popup grows to fit its options up to a built-in cap, then scrolls. Set max_visible_items= to cap it at roughly that many option rows — handy for long lists. Group headers and separators count toward the height, so the number is approximate.

countries = ["Canada", "France", "Germany", "Japan", "United States", "..."]
bs.Select(countries, label="Country", max_visible_items=8)

Label and message#

bs.Select(
    ["Option A", "Option B", "Option C"],
    label="Choose an option",
    message="Select the option that best applies.",
)

Required#

bs.Select(["Red", "Green", "Blue"], label="Color", required=True)

Searchable#

Set searchable=True to filter options as the user types.

countries = ["Canada", "France", "Germany", "Japan", "United States"]
bs.Select(countries, label="Country", searchable=True)

Custom values#

Set allow_custom_values=True to accept typed values not in the list.

bs.Select(["Red", "Green", "Blue"], allow_custom_values=True)

States#

bs.Select(["A", "B", "C"], value="A", label="Normal")
bs.Select(["A", "B", "C"], value="A", label="Read only",  read_only=True)
bs.Select(["A", "B", "C"], value="A", label="Disabled",   disabled=True)
Select states — light theme Select states — dark theme

Reactive binding#

Bind a Signal[str] with signal=. The field and signal stay in sync.

color = bs.Signal("Red")
bs.Select(["Red", "Green", "Blue"], signal=color)
color.subscribe(lambda v: apply_color(v))

Updating options at runtime#

Assign to .options to replace the list dynamically.

sel = bs.Select(["A", "B", "C"])
sel.options = ["X", "Y", "Z"]

Widget sizing#

All widgets accept self-placement kwargs via **kwargs. The parent container determines which options apply — stack-based parents use stack kwargs, grid-based parents use grid kwargs. Unrecognised keys are silently ignored.

Stack#

Used inside VStack, HStack, App, and other stack containers.

fill

Fill direction: 'x', 'y', 'both', or 'none'.

expand

Grow to consume extra space in the parent. True or False.

anchor

Alignment when the widget does not fill the available slot: 'n', 's', 'e', 'w', 'center', 'nw', etc.

margin

External spacing in pixels. Accepts an integer (equal on all sides), a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing (left and right). Accepts an integer or a 2-tuple (left, right) for asymmetric spacing. Overrides the horizontal component of margin=.

margin_y

Vertical external spacing (top and bottom). Accepts an integer or a 2-tuple (top, bottom) for asymmetric spacing. Overrides the vertical component of margin=.

Grid#

Used inside a Grid container.

row / column

Zero-based row and column indices.

rowspan / columnspan

Number of rows or columns to span.

sticky

Alignment and fill within the grid cell. Any combination of 'n', 's', 'e', 'w' — e.g. 'ew' stretches horizontally, 'nsew' fills the entire cell.

margin

External spacing in pixels. Accepts an integer, a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing. Accepts an integer or (left, right).

margin_y

Vertical external spacing. Accepts an integer or (top, bottom).

API#

The complete reference for Select lives on the Widgets API page. At a glance:

Select

A single-selection dropdown field.

Full Example#

 1
 2COUNTRIES = [
 3    "Canada", "France", "Germany", "Italy", "Japan",
 4    "Mexico", "Spain", "United Kingdom", "United States",
 5]
 6
 7with bs.App(title="Select Demo", padding=20, gap=16) as app:
 8
 9    # Basic
10    bs.Label("Basic", font="heading-sm")
11    bs.Select(["Red", "Green", "Blue"], fill="x")
12    bs.Select(["Red", "Green", "Blue"], value="Green", fill="x")
13
14    # Label and message
15    bs.Label("Label and Message", font="heading-sm")
16    bs.Select(
17        ["Option A", "Option B", "Option C"],
18        label="Choose an option",
19        message="Select the option that best applies.",
20        fill="x",
21    )
22
23    # Required
24    bs.Label("Required", font="heading-sm")
25    bs.Select(["Red", "Green", "Blue"], label="Color", required=True, fill="x")
26
27    # Searchable
28    bs.Label("Searchable", font="heading-sm")
29    bs.Select(COUNTRIES, label="Country", searchable=True, fill="x")
30
31    # Custom values
32    bs.Label("Custom Values", font="heading-sm")
33    bs.Select(
34        ["Red", "Green", "Blue"],
35        label="Color (custom allowed)",
36        allow_custom_values=True,
37        fill="x",
38    )
39
40    # Grouping — cluster the popup under headers by an option field
41    bs.Label("Grouping", font="heading-sm")
42    bs.Select(
43        options=[
44            {"text": "Apple",    "value": "apple",    "category": "Fruit"},
45            {"text": "Banana",   "value": "banana",   "category": "Fruit"},
46            {"text": "Carrot",   "value": "carrot",   "category": "Vegetable"},
47            {"text": "Broccoli", "value": "broccoli", "category": "Vegetable"},
48            {"text": "Basil",    "value": "basil",    "category": "Herb"},
49        ],
50        group_by="category",
51        label="Ingredient",
52        fill="x",
53    )
54
55    # Capped popup height — long list scrolls after ~6 rows
56    bs.Label("Limited Popup Height", font="heading-sm")
57    bs.Select(COUNTRIES, label="Country (max 6 visible)", max_visible_items=6, fill="x")
58
59    # States
60    bs.Label("States", font="heading-sm")
61    with bs.HStack(gap=8, fill="x", fill_items="x", expand_items=True):
62        bs.Select(["A", "B", "C"], value="A", label="Normal")
63        bs.Select(["A", "B", "C"], value="A", label="Read only",  read_only=True)
64        bs.Select(["A", "B", "C"], value="A", label="Disabled",   disabled=True)
65
66    # Reactive binding
67    bs.Label("Reactive Binding", font="heading-sm")
68    with bs.VStack(gap=6, fill="x"):
69        color = bs.Signal("Red")
70        bs.Select(["Red", "Green", "Blue"], label="Color", signal=color, fill="x")
71        color_lbl = bs.Label("Selected: Red", accent="secondary", font="caption")
72        color.subscribe(lambda v: setattr(color_lbl, 'text', f"Selected: {v}"))
73
74    # Runtime option updates
75    bs.Label("Runtime Updates", font="heading-sm")
76    with bs.VStack(gap=6, fill="x"):
77        sel = bs.Select(["Alpha", "Beta", "Gamma"], fill="x")
78        with bs.HStack(gap=8):
79            bs.Button("Set ABC", variant="outline",
80                on_click=lambda: setattr(sel, 'options', ["A", "B", "C"]))
81            bs.Button("Set 1-2-3", variant="outline",
82                on_click=lambda: setattr(sel, 'options', ["1", "2", "3"]))
83
84app.run()