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
rx.table.rootrx.table.headerrx.table.rowrx.table.column_header_cellrx.table.bodyrx.table.cellrx.table.row_header_cellDocumentación oficial
Pedido del taller
Haz una tabla con la lista de miembros para la pantalla de gestión del equipo. Cada fila lleva avatar, nombre con enlace, correo y un badge con el rol.
rx.table es una tabla que se arma a mano con su cabecera, sus filas y sus celdas. En una celda cabe cualquier componente (avatares, enlaces, botones), así que encaja con listas cuyo aspecto quieres decidir tú. Si necesitas una tabla que ya traiga ordenación, búsqueda y paginación, Data Table puede ser más rápido.
table.root: la tabla entera.table.header: la cabecera (<thead>). Dentro van table.row y table.column_header_cell (las celdas de título de columna).table.body: el cuerpo (<tbody>). Dentro van las table.row.table.row: una fila.table.row_header_cell: la primera celda que representa a la fila (el título de fila, en negrita).table.cell: una celda normal.rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Danilo Sousa"),
rx.table.cell("danilo@example.com"),
rx.table.cell("Developer"),
),
rx.table.row(
rx.table.row_header_cell("Zahra Ambessa"),
rx.table.cell("zahra@example.com"),
rx.table.cell("Admin"),
),
rx.table.row(
rx.table.row_header_cell("Jasper Eriks"),
rx.table.cell("jasper@example.com"),
rx.table.cell("Developer"),
),
),
width="100%",
)Conviene ajustar la tabla a su contenedor con width. Si hay tantas columnas que se desborda, envuélvela así: rx.box(rx.table.root(...), overflow_x="auto", width="100%"); de este modo hace scroll lateral la tabla, no la página.
variant y size van en table.root; justify, la alineación horizontal del contenido, va en cada celda. Las columnas numéricas suelen alinearse a la derecha (end).
Prueba las propiedades
variant
size
justify
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Item"),
rx.table.column_header_cell("Qty", justify="end"),
rx.table.column_header_cell("Price", justify="end"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Coffee beans"),
rx.table.cell("2", justify="end"),
rx.table.cell("$24.00", justify="end"),
),
rx.table.row(
rx.table.row_header_cell("Paper filters"),
rx.table.cell("1", justify="end"),
rx.table.cell("$5.50", justify="end"),
),
),
variant="ghost",
size="2",
width="100%",
)| Parte | Propiedad | Valores | Uso |
|---|---|---|---|
root | variant | "surface" "ghost" | Tabla con borde y fondo, o solo con líneas |
root | size | "1" "2" "3" | Relleno de las celdas y tamaño del texto |
root | align · summary | "left" "center" "right" · cadena | Alineación de la tabla, resumen de la tabla (atributos HTML) |
row | align | "start" "center" "end" "baseline" | Alineación vertical de las celdas de la fila |
column_header_cell · cell · row_header_cell | justify | "start" "center" "end" | Alineación horizontal del contenido de la celda |
| Las tres celdas | col_span · row_span | Entero | Celda que une varias columnas o filas |
| Las tres celdas | min_width · max_width | Longitud CSS | Ancho mínimo y máximo de la columna |
cell · row_header_cell | p px py pt pr pb pl | "0" ~ "9" | Relleno interior de la celda (escala de espaciado del tema) |
column_header_cell · row_header_cell | scope | "col" "row", etc. | Alcance al que se refiere la celda de cabecera (atributo HTML) |
header y body no tienen propiedades propias. El align de las celdas es el antiguo atributo align de HTML; para la alineación de Radix se usa justify.
col_span=2 hace que una celda ocupe dos columnas, y row_span=2, dos filas. El align de una fila decide la posición vertical de sus celdas.
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Day"),
rx.table.column_header_cell("Morning"),
rx.table.column_header_cell("Afternoon"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Mon"),
rx.table.cell("Workshop", col_span=2, justify="center"),
),
rx.table.row(
rx.table.row_header_cell("Tue"),
rx.table.cell("Review", row_span=2),
rx.table.cell("Demo"),
align="center",
),
rx.table.row(
rx.table.row_header_cell("Wed"),
rx.table.cell("Q&A"),
),
),
variant="surface",
width="100%",
)Si el contenido de la tabla tiene que cambiar, pon los datos en el State y dibuja las filas con rx.foreach. Con una función aparte que dibuje una fila, el código de la tabla queda más corto. Los datos de cada fila también pueden ser un dict o un dataclass en lugar de una lista (con un dataclass se accede por nombre, como person.email).
class TableForEachState(rx.State):
people: list[list[str]] = [
["Danilo Sousa", "danilo@example.com", "Developer"],
["Zahra Ambessa", "zahra@example.com", "Admin"],
["Jasper Eriks", "jasper@example.com", "Developer"],
]
def table_person_row(person: list):
return rx.table.row(
rx.table.cell(person[0]),
rx.table.cell(person[1]),
rx.table.cell(person[2]),
)
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(rx.foreach(TableForEachState.people, table_person_row)),
width="100%",
)Si los datos están en memoria, se ordenan y filtran con sorted() y listas por comprensión de Python. El original se guarda en una variable solo de backend que empieza por guion bajo, como _people, para no enviarlo al navegador, y la lista que se muestra es la variable calculada current_people. Con cache=True solo se recalcula cuando cambia el criterio de orden o el texto de búsqueda. Al on_change del campo de búsqueda se le añade .debounce(300) para que envíe el evento cuando se deja de escribir, no con cada tecla.
class TableSearchState(rx.State):
_people: list[dict[str, str]] = [
{"full_name": "Danilo Sousa", "email": "danilo@example.com", "group": "Developer"},
{"full_name": "Zahra Ambessa", "email": "zahra@example.com", "group": "Admin"},
{"full_name": "Jasper Eriks", "email": "zjasper@example.com", "group": "B-Developer"},
{"full_name": "Alice Brown", "email": "alice@example.com", "group": "Admin"},
]
sort_value: str = ""
search_value: str = ""
@rx.event
def set_sort_value(self, value: str):
self.sort_value = value
@rx.event
def set_search_value(self, value: str):
self.search_value = value
@rx.var(cache=True)
def current_people(self) -> list[dict[str, str]]:
people = self._people
if self.sort_value != "":
people = sorted(people, key=lambda p: p[self.sort_value].lower())
if self.search_value != "":
term = self.search_value.lower()
people = [p for p in people if any(term in v.lower() for v in p.values())]
return people
def table_search_row(person: dict):
return rx.table.row(
rx.table.cell(person["full_name"]),
rx.table.cell(person["email"]),
rx.table.cell(person["group"]),
)
rx.vstack(
rx.flex(
rx.select(
["full_name", "email", "group"],
placeholder="Sort By: full_name",
on_change=TableSearchState.set_sort_value,
),
rx.input(
placeholder="Search here...",
on_change=TableSearchState.set_search_value.debounce(300),
),
spacing="3",
wrap="wrap",
),
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(rx.foreach(TableSearchState.current_people, table_search_row)),
width="100%",
),
width="100%",
)Si los datos están en una base de datos, la misma pantalla se construye con una consulta SQL. Resumiendo el ejemplo de la documentación oficial, queda así (en esta página no se ejecuta). ilike es una coincidencia parcial que no distingue mayúsculas, y los % a ambos lados del texto significan «cualquier cosa».
from sqlmodel import asc, or_, select
class Customer(rx.Model, table=True):
name: str
email: str
phone: str
address: str
class DatabaseTableState(rx.State):
users: list[Customer] = []
sort_value: str = ""
search_value: str = ""
@rx.event
def load_entries(self):
with rx.session() as session:
query = select(Customer)
if self.search_value:
pattern = f"%{self.search_value.lower()}%"
query = query.where(or_(Customer.name.ilike(pattern), Customer.email.ilike(pattern)))
if self.sort_value:
query = query.order_by(asc(getattr(Customer, self.sort_value)))
self.users = session.exec(query).all()Cuantos más datos haya, mejor filtrar en la base de datos; si son pocos o vienen de una API, filtrar en memoria es más sencillo.
Para mostrar solo unas cuantas filas cada vez, guarda en el State offset (cuántas filas saltar) y limit (cuántas mostrar), y mete en users solo ese trozo. El on_mount de table.root carga los primeros datos. Con una base de datos, en lugar de recortar se usa query.offset(self.offset).limit(self.limit).
Page 1 / 1
class TablePageState(rx.State):
_customers: list[dict[str, str]] = [
{"name": "John Doe", "email": "john@example.com", "phone": "555-1234"},
{"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678"},
{"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012"},
{"name": "Alice Brown", "email": "alice@example.com", "phone": "555-3456"},
{"name": "Charlie Wilson", "email": "charlie@example.com", "phone": "555-7890"},
{"name": "Emily Davis", "email": "emily@example.com", "phone": "555-2345"},
{"name": "Frank Miller", "email": "frank@example.com", "phone": "555-6789"},
]
users: list[dict[str, str]] = []
total_items: int = 0
offset: int = 0
limit: int = 3
@rx.var
def page_number(self) -> int:
return self.offset // self.limit + 1
@rx.var
def total_pages(self) -> int:
return max(1, -(-self.total_items // self.limit))
@rx.event
def load_entries(self):
self.total_items = len(self._customers)
self.users = self._customers[self.offset : self.offset + self.limit]
@rx.event
def prev_page(self):
self.offset = max(self.offset - self.limit, 0)
self.load_entries()
@rx.event
def next_page(self):
if self.offset + self.limit < self.total_items:
self.offset += self.limit
self.load_entries()
def table_page_row(user: dict):
return rx.table.row(
rx.table.cell(user["name"]),
rx.table.cell(user["email"]),
rx.table.cell(user["phone"]),
)
rx.vstack(
rx.hstack(
rx.button("Prev", on_click=TablePageState.prev_page, variant="soft"),
rx.text("Page ", TablePageState.page_number, " / ", TablePageState.total_pages),
rx.button("Next", on_click=TablePageState.next_page, variant="soft"),
align="center",
),
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Phone"),
),
),
rx.table.body(rx.foreach(TablePageState.users, table_page_row)),
on_mount=TablePageState.load_entries,
width="100%",
),
width="100%",
)Si en una página hay varias tablas paginadas, cada una necesita su propio offset y limit. Cuando cambien la búsqueda o el orden, devuelve offset a 0 para no quedarte en una página más allá del final de los resultados reducidos. Para reutilizar la misma tabla varias veces, rx.ComponentState permite que cada tabla tenga su propio State.
Con rx.download puedes ofrecer los datos de la tabla como archivo descargable. Para JSON, el frontend pasa directamente el valor del State; los valores que no son cadenas se guardan convertidos a texto JSON. Para CSV, el handler genera el texto con el módulo csv y devuelve rx.download. En ambos casos el archivo lo recibe el navegador del usuario y en el servidor no se guarda nada.
import csv
import io
import json
class TableDownloadState(rx.State):
users: list[dict[str, str]] = [
{"name": "John Doe", "email": "john@example.com", "phone": "555-1234"},
{"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678"},
{"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012"},
]
@rx.event
def download_csv_data(self):
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=["name", "email", "phone"])
writer.writeheader()
writer.writerows(self.users)
return rx.download(data=output.getvalue(), filename="data.csv")
def table_download_row(user: dict):
return rx.table.row(
rx.table.cell(user["name"]),
rx.table.cell(user["email"]),
rx.table.cell(user["phone"]),
)
rx.vstack(
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Phone"),
),
),
rx.table.body(rx.foreach(TableDownloadState.users, table_download_row)),
width="100%",
),
rx.hstack(
rx.button("Download as JSON", on_click=rx.download(data=TableDownloadState.users, filename="data.json")),
rx.button("Download as CSV", on_click=TableDownloadState.download_csv_data),
spacing="3",
wrap="wrap",
),
width="100%",
spacing="4",
)Una lista de miembros solo con cuerpo, sin fila de cabecera. La primera celda lleva el avatar, la celda de título de fila el nombre con enlace y la última el badge del rol, y cada fila usa align="center" para alinear verticalmente el avatar y el texto. Para que la página no se desborde en pantallas estrechas, la tabla va envuelta en una caja con overflow_x="auto".
Invite and manage your team members
rx.flex(
rx.heading("Your Team", as_="h2", size="5"),
rx.text("Invite and manage your team members"),
rx.flex(
rx.input(placeholder="Email Address"),
rx.button("Invite"),
spacing="2",
),
rx.box(
rx.table.root(
rx.table.body(
rx.table.row(
rx.table.cell(rx.avatar(fallback="DS")),
rx.table.row_header_cell(rx.link("Danilo Sousa")),
rx.table.cell("danilo@example.com"),
rx.table.cell(rx.badge("Developer")),
align="center",
),
rx.table.row(
rx.table.cell(rx.avatar(fallback="ZA")),
rx.table.row_header_cell(rx.link("Zahra Ambessa")),
rx.table.cell("zahra@example.com"),
rx.table.cell(rx.badge("Admin", color_scheme="orange")),
align="center",
),
rx.table.row(
rx.table.cell(rx.avatar(fallback="JE")),
rx.table.row_header_cell(rx.link("Jasper Eriksson")),
rx.table.cell("jasper@example.com"),
rx.table.cell(rx.badge("Developer")),
align="center",
),
),
width="100%",
),
overflow_x="auto",
width="100%",
),
width="100%",
direction="column",
spacing="2",
)Notas del taller
root > header(row > column_header_cell) + body(row > row_header_cell · cell).
variant (surface/ghost) y size en root; justify · col_span · row_span en las celdas; align en las filas.
Datos que cambian: State + rx.foreach. El original en una variable _de backend; la lista visible, en una variable calculada.
Ordenación, búsqueda y paginación se hacen a mano. Si necesitas una tabla que lo traiga todo, usa rx.data_table.
Envuelve las tablas anchas en una caja con overflow_x="auto" para que solo haga scroll la tabla.
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
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.