docs/library/forms/select.md
import random
import reflex as rx
A select component displays a dropdown list of options for the user to pick one from. It's one of the most common form controls in web applications, use it whenever you need a user to choose a single value from a predefined list and screen space is limited.
Reflex's rx.select is a fully-featured, accessible dropdown built on Radix UI primitives. It works with simple option lists or full state-bound bindings, supports form integration, keyboard navigation, and works out-of-the-box on desktop and mobile.
Use rx.select when:
Consider alternatives when:
At its simplest, pass a list of string options. The component renders a dropdown button that opens a menu of choices.
rx.select(["apple", "grape", "pear"])
The user can click the trigger button to open the dropdown and choose a different option.
In most real apps, you need to react when the user selects a value, to filter data, update a chart, save to a database, or trigger another event. Bind the select to a State var using the value prop and an on_change event handler.
class SelectState(rx.State):
selected_fruit: str = "apple"
@rx.event
def update_fruit(self, value: str):
self.selected_fruit = value
def select_with_state():
return rx.hstack(
rx.select(
["apple", "grape", "pear"],
value=SelectState.selected_fruit,
on_change=SelectState.update_fruit,
),
rx.text("You selected:"),
rx.badge(SelectState.selected_fruit),
align="center",
spacing="3",
)
This is the most common pattern. Because the select is bound to a state var, the value always reflects what's stored there, you can update it from anywhere in your app, and the select will re-render automatically.
If you just need the select to start with a specific option chosen, without tracking the user's choice in state, use default_value. This is the simplest pattern when you only care about the value at form submission time, or when you don't need the selected value to affect anything else in your app.
rx.select(
["apple", "grape", "pear"],
default_value="grape",
)
When you want the select to start empty, prompting the user to make a choice, omit default_value and provide a placeholder.
rx.select(
["apple", "grape", "pear"],
placeholder="Choose a fruit…",
)
Real applications rarely have hardcoded options. More often, options come from a database, API, or calculated from other state. Pass a state var as the options list, and the dropdown updates whenever the list changes.
class SelectStateDynamic(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]]
def select_dynamic():
return rx.vstack(
rx.select(
SelectStateDynamic.options,
value=SelectStateDynamic.selected,
on_change=SelectStateDynamic.set_selected,
),
rx.hstack(
rx.button("Pick Random", on_click=SelectStateDynamic.randomize),
rx.button("Add Option", on_click=SelectStateDynamic.add_option),
spacing="2",
),
spacing="3",
)
To prevent user interaction, set disabled=True. The select renders with a muted appearance and cannot be opened.
rx.select(
["apple", "grape", "pear"],
default_value="apple",
disabled=True,
)
To disable individual items rather than the whole select, use the low-level API and set disabled=True on specific rx.select.item components.
Select components integrate cleanly with Reflex forms. The name prop sets the key used when the form is submitted, and required=True prevents submission until the user makes a choice.
class SelectFormState(rx.State):
form_data: dict = {}
@rx.event
def handle_submit(self, form_data: dict):
self.form_data = form_data
def select_form():
return rx.card(
rx.vstack(
rx.heading("Order Form", size="4"),
rx.form.root(
rx.vstack(
rx.text("Favorite fruit"),
rx.select(
["apple", "grape", "pear"],
name="fruit",
placeholder="Pick one",
required=True,
),
rx.text("Quantity"),
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.heading("Results:"),
rx.badge(SelectFormState.form_data.to_string()),
),
spacing="3",
width="100%",
align_items="left",
),
width="400px",
)
For full details on building forms, validation, and submission handling, see the Form documentation.
When your options have separate display labels and underlying values (e.g., a user ID for the value, a name for the label), use a computed var to map between them.
class SelectDictState(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
def select_dict_example():
return rx.vstack(
rx.select(
SelectDictState.user_names,
value=SelectDictState.selected_name,
on_change=SelectDictState.set_user,
),
rx.text("Selected user ID:"),
rx.badge(SelectDictState.selected_id),
spacing="3",
)
For native label/value separation in the dropdown itself, use the low-level Select API and pass value= and a display label child to each rx.select.item.
When placing a select inside a Dialog or other portal-based container, set position="popper" on the select so the dropdown menu positions itself correctly above the overlay content.
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")),
spacing="3",
),
),
)
Beyond on_change, the on_open_change event fires when the dropdown opens or closes. Use this to trigger analytics, prefetch data, or animate related UI.
The example below uses rx.cond to swap between two badges based on whether the dropdown is open.
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
def select_open_change():
return rx.vstack(
rx.select(
["apple", "grape", "pear"],
default_value="apple",
on_open_change=SelectOpenState.on_toggle,
),
rx.text("Open count: ", SelectOpenState.open_count),
rx.cond(
SelectOpenState.is_open,
rx.badge("Open", color_scheme="green"),
rx.badge("Closed", color_scheme="gray"),
),
spacing="3",
)
A classic use case, the select controls what data is displayed elsewhere on the page.
class FilterState(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]
def select_filter():
return rx.vstack(
rx.select(
["laptops", "phones", "tablets"],
value=FilterState.category,
on_change=FilterState.set_category,
),
rx.foreach(
FilterState.filtered_items,
lambda item: rx.card(item["name"]),
),
spacing="3",
width="300px",
)
When one select's options depend on another's value, a common pattern for country/state, category/subcategory, etc.
class CascadeState(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, [])
def select_cascade():
return rx.hstack(
rx.select(
CascadeState.region_names,
value=CascadeState.region,
on_change=CascadeState.set_region,
),
rx.select(
CascadeState.countries,
value=CascadeState.country,
on_change=CascadeState.set_country,
),
spacing="3",
)
For prototypes and quick data scripts, st.selectbox is fine. But for production apps, you need a dropdown component that doesn't trigger a full-page rerun on every interaction. rx.select is the most direct replacement, same API simplicity, but built on Radix UI as a real React component bound to Python state. It supports proper form integration, keyboard navigation, and accessibility out of the box, and works in apps with authentication, routing, and complex state.
Use rx.select(["option_1", "option_2", "option_3"]). That's the full code for a working dropdown, Reflex compiles your Python to a React app, so the menu renders as a styled, accessible web component without you writing any JavaScript, HTML, or CSS. For interactive dropdowns bound to your app state, add value= and on_change= props pointing to a state var and event handler.
Plotly Dash's dcc.Dropdown works but requires the callback decorator pattern, every interaction needs a @app.callback with explicit inputs and outputs. rx.select does the same job with standard Python event handlers: define a method on your state class, pass it to on_change=, and Reflex handles the wiring. Migration is usually mechanical: replace dcc.Dropdown(options=..., value=..., id=...) with rx.select(options, value=State.value, on_change=State.set_value) and delete the @app.callback decorators.
Because that's how Streamlit works by default, every widget interaction reruns your entire script from the top. This is fine for small scripts but becomes a performance problem as your app grows. The alternatives are: use st.cache_data and st.session_state aggressively to limit recomputation, switch to fragments (@st.fragment) for isolated reruns, or move to a framework like Reflex where state updates trigger granular re-renders instead of full reruns. Reflex's rx.select only updates the UI elements that actually depend on the selected value.
For prototypes, st.selectbox (Streamlit), gr.Dropdown (Gradio), and dcc.Dropdown (Plotly Dash) all work fine. For production apps with auth, routing, and complex state, rx.select (Reflex) is purpose-built, it's a real React component under the hood, supports proper form integration, and doesn't suffer from the full-page rerun problem that limits Streamlit at scale. The choice often comes down to whether you're building a quick demo or a real product.
Bind the dropdown to a state variable, then derive the filtered data using a computed property. In Reflex: rx.select(categories, value=State.selected, on_change=State.set_selected) paired with a @rx.var filtered_rows(self) -> list: return [r for r in self.all_rows if r.category == self.selected]. The pattern is essentially the same in Streamlit, Dash, and Gradio, but Reflex's version only re-renders the table, not the entire page.
Reflex is the most popular open-source alternative to Retool for building internal tools and admin panels. Unlike Retool, which is a hosted low-code platform with seat-based pricing, Reflex is open source, self-hostable, and lets you build everything in Python, including dropdowns (rx.select), tables, forms, and complete CRUD workflows. You get the same component-based UX as Retool, but with full code ownership and no per-user fees.
Load the values into a state variable when the page loads, then pass the variable as the options list. In a Reflex app: define options: list[str] = [] on a State class, populate it in an on_mount handler that queries your database, and pass State.options to rx.select. The dropdown updates automatically whenever the underlying data changes. The same pattern works with any database, SQLAlchemy, raw SQL, an ORM, or an API call.
Native <select> elements don't support search. For a searchable dropdown, sometimes called a combobox or typeahead, you need a custom implementation. In Reflex, this is done via the low-level Select API combined with an input field that filters the options list. Streamlit's st.selectbox added basic search behavior, and Dash has dcc.Dropdown(searchable=True), but both are limited in customization compared to building it yourself with composable primitives.
A dropdown (or single-select) lets the user pick one option. A multiselect lets them pick several. In native HTML these are different elements (<select> vs <select multiple>). In Python frameworks: Streamlit uses st.selectbox and st.multiselect, Dash uses dcc.Dropdown(multi=True), and Reflex uses rx.select for single and rxe.mantine.multi_select (Reflex Enterprise) for multi. Choose based on whether your data model allows one or many values per field.
on_change, on_open_change, and related triggers