R

ReflexKR

Documentación de Reflex

El taller de componentes: los 100 componentes de Reflex

·

Resumen del curso

Diseño

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Tipografía

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Formularios

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Visualización de datos

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Mostrar y ocultar

·

Tabs

·

Accordion

·

Segmented Control

Superposiciones

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Renderizado dinámico

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Multimedia

·

Image

·

Audio

·

Video

Tablas y cuadrículas de datos

·

Table

·

Data Table

·

Data Editor

Gráficos

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Piezas de gráficos

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Otras bibliotecas de gráficos

·

Plotly

·

Pyplot

Elementos HTML

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Otras herramientas

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

El taller de componentes: los 100 componentes de Reflex
Formularios

Select

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

Documentación oficial

Pedido del taller

Pon un desplegable de categorías encima de la lista de productos. Debajo solo deben aparecer los productos de la categoría elegida.

rx.select es un desplegable para elegir un valor de una lista que se abre al pulsarlo. Está construido sobre Radix UI, así que también se puede usar con el teclado y funciona igual en escritorio y en móvil. Basta con pasarle una lista para que cree el botón y la lista a la vez.

  • Se usa cuando hay más de cinco opciones y hay que ahorrar espacio vertical, o cuando hay que elegir sí o sí una de una lista predefinida.
  • Con pocas opciones es mejor Radio Group, porque se ven todas de un vistazo. Para elegir varias, un grupo de Checkbox; si hay que escribir el valor, Input.
  • Para una composición más fina, como títulos de grupo, separadores o bloquear una opción, se usa la API de bajo nivel (rx.select.root …).
rx.select(["apple", "grape", "pear"])

Prueba las propiedades

placeholder solo se ve cuando no se ha elegido nada. Con position="popper" la lista se abre debajo del botón; con "item-aligned" (por defecto), se abre de modo que la opción elegida quede encima del botón.

Prueba las propiedades

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 tiene además ghost, que quita incluso el borde del botón. high_contrast aumenta el contraste de la lista desplegada.
PropiedadValoresPor defectoUso
Primer argumento (items)Lista de cadenas / VarObligatorioLista de opciones. Valor y texto visible coinciden
valueCadena / VarNingunoSelección controlada (junto con on_change)
default_valueCadenaNingunoValor elegido solo al principio
placeholderCadena""Texto de ayuda en el botón antes de elegir
labelCadenaNingunoTítulo de grupo en lo alto de la lista desplegada
variant"classic" "surface" "soft" "ghost""surface"Aspecto del botón
size"1" ~ "3""2"Tamaño
color_scheme, radius, high_contrastValor del temaColor, esquinas del botón, contraste de la lista
position"item-aligned" "popper""item-aligned"Cómo se abre la lista. Dentro de un diálogo, popper
widthLongitud CSSNingunoAncho del botón
open / default_openTrue / FalseFalseControlar si la lista está abierta / abrirla desde el principio
name, required, disabledNombre para el envío, hay que elegir para enviar, bloquear
on_changeEventoAl elegir otra opción. Pasa el valor (cadena)
on_open_changeEventoAl abrirse y cerrarse la lista. Pasa True/False

Seguir el valor elegido

Es el uso más habitual. Conecta a value una variable del State y a on_change un handler que la actualice. La selección muestra siempre el valor del State, así que el desplegable cambia aunque el valor se modifique desde cualquier parte de la app.

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",
)

Valor inicial, texto de ayuda y bloqueo

Si no necesitas State y solo quieres fijar la selección inicial, usa default_value (no controlado). Si el valor solo hace falta al enviar el formulario, con esto basta. Para empezar vacío e invitar a elegir, quita default_value y pon placeholder. Con disabled=True se ve atenuado y no se abre.

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",
)

Opciones que vienen del State

Normalmente las opciones vienen de una base de datos o de una API. Si en lugar de una lista pasas un Var de lista del State, la lista de opciones cambia cuando cambia esa lista. Internamente, las opciones se crean con 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"),
)

En un formulario

name es la clave de los datos enviados y, con required=True, no se envía hasta que se elige algo. Un select sin elegir entra como valor vacío ("").

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",
)

Separar el nombre visible y el valor

En el rx.select de alto nivel, el valor y el texto visible coinciden. Si quieres mostrar nombres de usuario pero usar su ID, puedes buscar el ID a partir del nombre con una variable calculada, como hace la documentación oficial.

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"),
)

Para separarlos dentro del propio desplegable, usa la API de bajo nivel: rx.select.item("texto visible", value="valor"). on_change pasa el 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",
)

Dentro de un diálogo

Dentro de un contenedor que flota sobre la pantalla, como Dialog, pon position="popper". Así la lista no queda cortada por el diálogo y se abre justo debajo del botón.

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",
    ),
)

Al abrirse y cerrarse: on_open_change

on_open_change pasa True cuando la lista se abre y False cuando se cierra. Sirve para precargar datos al abrirla o para contar cuántas veces se abre.

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",
    ),
)

API de bajo nivel: subcomponentes

El rx.select de alto nivel no es más que un montaje automático de las piezas de abajo. Si lo montas tú, puedes añadir títulos de grupo y separadores, y bloquear una sola opción.

PiezaFunciónPropiedades principales
rx.select.rootEl conjunto y la selecciónvalue default_value on_change open default_open on_open_change name required disabled size
rx.select.triggerEl botón que se pulsaplaceholder variant color_scheme radius
rx.select.contentLa lista desplegadaposition side side_offset align align_offset variant ("solid" "soft") color_scheme high_contrast
rx.select.groupGrupo de opciones
rx.select.labelTítulo del grupo
rx.select.itemUna opciónvalue (obligatorio) disabled
rx.select.separatorSeparador

rx.select.content también tiene los eventos on_escape_key_down, on_pointer_down_outside y on_close_auto_focus. Abajo hay una lista con dos grupos, un separador y una opción bloqueada (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",
)

Control total: value y open

También con el root de bajo nivel, el State puede controlar la selección con value + on_change y la apertura con open + on_open_change. Así se puede cambiar la selección o abrir la lista desde un botón. Ojo: si on_open_change no actualiza open, la lista no se abrirá ni se cerrará aunque pulses el botón.

En un formulario, el name del root es la clave y el value del item elegido es el valor.

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"),
    ),
)

Selección en cadena

Es el patrón en el que las opciones del segundo select dependen del valor del primero (región → país). Al cambiar la región, el handler ajusta también el país al primero de esa región, para que el segundo select nunca apunte a un valor que no existe.

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",
)

Completar el pedido

Es el patrón de filtro, en el que el select decide qué datos se muestran debajo. La variable calculada filtered_items devuelve solo los productos de la categoría elegida y rx.foreach los coloca como tarjetas. label aparece como título de grupo en lo alto de la lista desplegada.

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",
)

Notas del taller

  • Se crea con rx.select([opciones...]) y se une al State con value + on_change. Solo el valor inicial, default_value.

  • Texto de ayuda antes de elegir: placeholder; dentro de un diálogo, position="popper".

  • Si pasas las opciones como lista del State, la lista sigue a los datos.

  • Si el texto visible y el valor difieren, o necesitas grupos, separadores u opciones bloqueadas: root · trigger · content · group · label · item · separator.

  • on_open_change pasa abierto (True)/cerrado (False). Si controlas open tú mismo, el handler debe actualizar open.

← Anterior

Formularios · Radio Group

Siguiente →

Formularios · Slider

Recibe avisos de nuevas lecciones

Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.

Datos: tu correo electrónico · Finalidad: avisos de nuevas lecciones o de la publicación de este curso · Conservación: hasta que pidas que lo borremos. No lo usamos para nada más ni lo compartimos. Responde a cualquier aviso y lo borraremos enseguida.

Preguntas y respuestas

Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.

Se muestra como texto plano, sin enlaces ni formato.

R

ReflexKR

Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.

ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café

Aviso de afiliados
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex