R

ReflexKR

Reflex Docs

The Component Workshop: Reflex's 100 Library Components

·

Course overview

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typography

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Forms

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Data display

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Disclosure

·

Tabs

·

Accordion

·

Segmented Control

Overlay

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamic rendering

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Media

·

Image

·

Audio

·

Video

Tables and data grids

·

Table

·

Data Table

·

Data Editor

Charts

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Chart parts

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Other chart libraries

·

Plotly

·

Pyplot

HTML elements

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Other tools

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

The Component Workshop: Reflex's 100 Library Components
Forms

Select

rx.selectrx.select.rootrx.select.triggerrx.select.contentrx.select.grouprx.select.labelrx.select.itemrx.select.separator

Official docs

Workshop order

Put a category dropdown above the product list. Only products in the chosen category should show below it.

rx.select is a dropdown: click it and pick one value from the list that opens. It is built on Radix UI, so it works with the keyboard and behaves the same on desktop and mobile. Pass just a list and you get both the button and the list.

  • Use it when there are more than about five options and you need to save vertical space, or when the user must pick one item from a fixed list.
  • With few options, Radio Group is better because everything is visible at a glance. To pick several, use a group of Checkboxes; for free text, use Input.
  • For finer structure, such as group titles, separators or a single disabled option, use the low-level API (rx.select.root …).
rx.select(["apple", "grape", "pear"])

Try the props

placeholder only shows while nothing is selected. With position="popper" the list opens below the button; with "item-aligned" (the default) it opens so that the selected option sits over the button.

Try the props

placeholder

variant

size

color

radius

position

high_contrast

disabled

rx.select(["apple", "grape", "pear"], placeholder="Choose a fruit...", variant="surface", size="2", color_scheme="indigo", radius="medium", position="item-aligned", high_contrast=False, disabled=False)
variant has an extra option, ghost, which removes even the button border. high_contrast raises the contrast of the open list.
PropValuesDefaultUse
first argument (items)list of strings / VarrequiredOptions. Value and displayed text are the same
valuestring / VarnoneControlled selection (with on_change)
default_valuestringnoneSelection set only once at the start
placeholderstring""Hint shown on the button before a choice is made
labelstringnoneGroup title at the top of the open list
variant"classic" "surface" "soft" "ghost""surface"Button look
size"1" – "3""2"Size
color_scheme, radius, high_contrasttheme valueColor, button corners, list contrast
position"item-aligned" "popper""item-aligned"How the list opens. Use popper inside a dialog
widthCSS lengthnoneButton width
open / default_openTrue / FalseFalseControl whether the list is open / open it from the start
name, required, disabledForm field name, must pick to submit, disable
on_changeeventWhen a different option is picked. Passes the value (a string)
on_open_changeeventWhen the list opens or closes. Passes True/False

Tracking the selection

This is the most common use. Connect value to a State variable and on_change to a handler that updates it. The selection always shows the State value, so wherever in the app the value changes, the dropdown follows.

You selected:

apple
class SelectFruitState(rx.State):
    selected_fruit: str = "apple"

    @rx.event
    def update_fruit(self, value: str):
        self.selected_fruit = value


rx.hstack(
    rx.select(
        ["apple", "grape", "pear"],
        value=SelectFruitState.selected_fruit,
        on_change=SelectFruitState.update_fruit,
    ),
    rx.text("You selected:"),
    rx.badge(SelectFruitState.selected_fruit),
    align="center",
    spacing="3",
)

Initial value, placeholder, disabled

If you don't need State and just want to set the initial selection, use default_value (uncontrolled). That's enough when you only need the value on form submit. To start empty and invite a choice, drop default_value and give a placeholder. With disabled=True it is dimmed and won't open.

rx.flex(
    rx.select(["apple", "grape", "pear"], default_value="grape"),
    rx.select(["apple", "grape", "pear"], placeholder="Choose a fruit..."),
    rx.select(["apple", "grape", "pear"], default_value="apple", disabled=True),
    spacing="3",
    wrap="wrap",
)

Options from State

Options usually come from a database or an API. Pass a list Var from the State instead of a plain list, and the dropdown changes whenever the list does. Internally, the options are built with rx.foreach.

3 options

import random


class SelectDynamicState(rx.State):
    options: list[str] = ["apple", "grape", "pear"]
    selected: str = "apple"

    @rx.event
    def set_selected(self, value: str):
        self.selected = value

    @rx.event
    def randomize(self):
        self.selected = random.choice(self.options)

    @rx.event
    def add_option(self):
        new_options = ["banana", "orange", "mango", "kiwi", "cherry"]
        available = [o for o in new_options if o not in self.options]
        if available:
            self.options = self.options + [available[0]]

    @rx.event
    def reset_options(self):
        self.options = ["apple", "grape", "pear"]
        self.selected = "apple"


rx.vstack(
    rx.select(
        SelectDynamicState.options,
        value=SelectDynamicState.selected,
        on_change=SelectDynamicState.set_selected,
    ),
    rx.hstack(
        rx.button("Pick Random", on_click=SelectDynamicState.randomize, size="1"),
        rx.button("Add Option", on_click=SelectDynamicState.add_option, size="1", variant="soft"),
        rx.button("Reset", on_click=SelectDynamicState.reset_options, size="1", variant="soft", color_scheme="gray"),
    ),
    rx.text(SelectDynamicState.options.length(), " options", size="1", color_scheme="gray"),
)

Using it in a form

name becomes the key in the submitted data, and with required=True the form won't submit until something is picked. A select with no choice is sent as an empty value ("").

Favorite fruit

Quantity

Results:

{}
class SelectFormState(rx.State):
    form_data: dict = {}

    @rx.event
    def handle_submit(self, form_data: dict):
        self.form_data = form_data


rx.card(
    rx.vstack(
        rx.form.root(
            rx.vstack(
                rx.text("Favorite fruit", size="2"),
                rx.select(["apple", "grape", "pear"], name="fruit", placeholder="Pick one", required=True),
                rx.text("Quantity", size="2"),
                rx.select(["1", "2", "3", "4", "5"], name="quantity", default_value="1"),
                rx.button("Submit Order", type="submit"),
                spacing="2",
                align="stretch",
            ),
            on_submit=SelectFormState.handle_submit,
            reset_on_submit=True,
        ),
        rx.divider(),
        rx.hstack(rx.text("Results:", weight="bold"), rx.badge(SelectFormState.form_data.to_string())),
        spacing="3",
    ),
    width="320px",
)

Separating the displayed name from the value

In the high-level rx.select, the value and the displayed text are the same. If you need to show user names but work with IDs, you can look up the ID from the name with a computed var, as the official docs do.

Selected user ID: user_001

class SelectUserState(rx.State):
    users: dict[str, str] = {
        "user_001": "Alice Johnson",
        "user_002": "Bob Smith",
        "user_003": "Carol Davis",
    }
    selected_name: str = "Alice Johnson"

    @rx.var
    def user_names(self) -> list[str]:
        return list(self.users.values())

    @rx.var
    def selected_id(self) -> str:
        for uid, name in self.users.items():
            if name == self.selected_name:
                return uid
        return ""

    @rx.event
    def set_user(self, name: str):
        self.selected_name = name


rx.vstack(
    rx.select(
        SelectUserState.user_names,
        value=SelectUserState.selected_name,
        on_change=SelectUserState.set_user,
    ),
    rx.text("Selected user ID: ", rx.code(SelectUserState.selected_id), size="2"),
)

To separate them right in the dropdown, use the low-level API: rx.select.item("Displayed text", value="value"). on_change passes the value.

value = kr

class SelectCountryState(rx.State):
    code: str = "kr"

    @rx.event
    def set_code(self, value: str):
        self.code = value


rx.hstack(
    rx.select.root(
        rx.select.trigger(),
        rx.select.content(
            rx.select.item("Korea", value="kr"),
            rx.select.item("Japan", value="jp"),
            rx.select.item("Germany", value="de"),
        ),
        value=SelectCountryState.code,
        on_change=SelectCountryState.set_code,
    ),
    rx.text("value = ", rx.code(SelectCountryState.code), size="2"),
    align="center",
)

Inside a dialog

Inside a container floating above the page, such as Dialog, set position="popper". That way the list opens right below the button instead of being clipped by the dialog.

rx.dialog.root(
    rx.dialog.trigger(rx.button("Open Dialog")),
    rx.dialog.content(
        rx.dialog.title("Pick a fruit"),
        rx.vstack(
            rx.select(["apple", "grape", "pear"], position="popper", default_value="apple"),
            rx.dialog.close(rx.button("Close", variant="soft")),
            spacing="3",
        ),
        max_width="320px",
    ),
)

When it opens and closes: on_open_change

on_open_change passes True when the list opens and False when it closes. Use it to preload data on open or to count how often it is opened.

Open count: 0

Closed
class SelectOpenState(rx.State):
    is_open: bool = False
    open_count: int = 0

    @rx.event
    def on_toggle(self, is_open: bool):
        self.is_open = is_open
        if is_open:
            self.open_count += 1


rx.vstack(
    rx.select(["apple", "grape", "pear"], default_value="apple", on_open_change=SelectOpenState.on_toggle),
    rx.hstack(
        rx.text("Open count: ", SelectOpenState.open_count, size="2"),
        rx.cond(
            SelectOpenState.is_open,
            rx.badge("Open", color_scheme="green"),
            rx.badge("Closed", color_scheme="gray"),
        ),
        align="center",
    ),
)

Low-level API: subcomponents

The high-level rx.select simply assembles the parts below for you. Assemble them yourself and you can add group titles and separators, or disable a single option.

PartJobMain props
rx.select.rootThe whole thing and the selectionvalue default_value on_change open default_open on_open_change name required disabled size
rx.select.triggerThe button you clickplaceholder variant color_scheme radius
rx.select.contentThe open listposition side side_offset align align_offset variant ("solid" "soft") color_scheme high_contrast
rx.select.groupA group of options
rx.select.labelGroup title
rx.select.itemA single optionvalue (required) disabled
rx.select.separatorSeparator line

rx.select.content also has the events on_escape_key_down, on_pointer_down_outside and on_close_auto_focus. Below is a list with two groups, a separator and a disabled option (Grape).

rx.select.root(
    rx.select.trigger(placeholder="Pick a food", variant="soft", radius="full"),
    rx.select.content(
        rx.select.group(
            rx.select.label("Fruits"),
            rx.select.item("Orange", value="orange"),
            rx.select.item("Apple", value="apple"),
            rx.select.item("Grape", value="grape", disabled=True),
        ),
        rx.select.separator(),
        rx.select.group(
            rx.select.label("Vegetables"),
            rx.select.item("Carrot", value="carrot"),
            rx.select.item("Potato", value="potato"),
        ),
        position="popper",
        side="bottom",
        side_offset=6,
        variant="soft",
        color_scheme="green",
    ),
    size="2",
)

Full control: value and open

On the low-level root too, the State can hold the selection with value + on_change and the open state with open + on_open_change. Buttons can then change the selection or open the list. Be careful: if on_open_change doesn't update open, clicking the button neither opens nor closes the list.

In a form, the root's name is the key and the chosen item's value is the value.

class SelectControlState(rx.State):
    value: str = "apple"
    open: bool = False

    @rx.event
    def set_value(self, value: str):
        self.value = value

    @rx.event
    def set_open(self, value: bool):
        self.open = value

    @rx.event
    def next_fruit(self):
        fruits = ["apple", "grape", "pear"]
        self.value = fruits[(fruits.index(self.value) + 1) % 3]


rx.vstack(
    rx.select.root(
        rx.select.trigger(),
        rx.select.content(
            rx.select.item("apple", value="apple"),
            rx.select.item("grape", value="grape"),
            rx.select.item("pear", value="pear"),
        ),
        value=SelectControlState.value,
        on_change=SelectControlState.set_value,
        open=SelectControlState.open,
        on_open_change=SelectControlState.set_open,
    ),
    rx.hstack(
        rx.button("Next value", on_click=SelectControlState.next_fruit, size="1", variant="soft"),
        rx.button("Open menu", on_click=SelectControlState.set_open(True), size="1", variant="soft"),
    ),
)

Chained selects

In this pattern, the options of the second select depend on the value of the first (region → country). When the region changes, the handler also sets the country to the first country of that region, so the second select never points at a value that doesn't exist.

class SelectCascadeState(rx.State):
    regions: dict[str, list[str]] = {
        "North America": ["USA", "Canada", "Mexico"],
        "Europe": ["UK", "France", "Germany"],
        "Asia": ["Japan", "Korea", "Singapore"],
    }
    region: str = "North America"
    country: str = "USA"

    @rx.event
    def set_region(self, value: str):
        self.region = value
        self.country = self.regions[value][0]

    @rx.event
    def set_country(self, value: str):
        self.country = value

    @rx.var
    def region_names(self) -> list[str]:
        return list(self.regions.keys())

    @rx.var
    def countries(self) -> list[str]:
        return self.regions.get(self.region, [])


rx.hstack(
    rx.select(SelectCascadeState.region_names, value=SelectCascadeState.region, on_change=SelectCascadeState.set_region),
    rx.select(SelectCascadeState.countries, value=SelectCascadeState.country, on_change=SelectCascadeState.set_country),
    spacing="3",
    wrap="wrap",
)

Filling the order

This is the filter pattern, where a select decides which data shows below it. The computed var filtered_items returns only the products in the chosen category, and rx.foreach lays them out as cards. label appears as a group title at the top of the open list.

MacBook Pro
Dell XPS
class SelectFilterState(rx.State):
    all_items: list[dict[str, str]] = [
        {"name": "MacBook Pro", "category": "laptops"},
        {"name": "Dell XPS", "category": "laptops"},
        {"name": "iPhone 15", "category": "phones"},
        {"name": "Pixel 8", "category": "phones"},
        {"name": "iPad Air", "category": "tablets"},
        {"name": "Galaxy Tab", "category": "tablets"},
    ]
    category: str = "laptops"

    @rx.event
    def set_category(self, value: str):
        self.category = value

    @rx.var
    def filtered_items(self) -> list[dict[str, str]]:
        return [i for i in self.all_items if i["category"] == self.category]


rx.vstack(
    rx.select(
        ["laptops", "phones", "tablets"],
        value=SelectFilterState.category,
        on_change=SelectFilterState.set_category,
        label="Category",
    ),
    rx.foreach(SelectFilterState.filtered_items, lambda item: rx.card(item["name"], width="100%")),
    spacing="3",
    width="280px",
)

Workshop notes

  • Create it with rx.select([options...]) and bind it to the State with value + on_change. Set only the initial value with default_value.

  • Use placeholder for the hint before a choice, and position="popper" inside a dialog.

  • Pass the options as a State list and the dropdown follows the data.

  • When the displayed text differs from the value, or you need groups, separators or disabled options, use root, trigger, content, group, label, item and separator.

  • on_open_change reports open (True) / closed (False). If you control open yourself, the handler must update open.

← Previous

Forms · Radio Group

Next →

Forms · Slider

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

R

ReflexKR

Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.

ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee

Affiliate disclosure
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex