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
Tablas y cuadrículas de datos

Table

rx.table.rootrx.table.headerrx.table.rowrx.table.column_header_cellrx.table.bodyrx.table.cellrx.table.row_header_cell

Documentació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.
Full nameEmailGroup
Danilo Sousadanilo@example.comDeveloper
Zahra Ambessazahra@example.comAdmin
Jasper Eriksjasper@example.comDeveloper
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.

Prueba las propiedades

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

ItemQtyPrice
Coffee beans2$24.00
Paper filters1$5.50

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%",
)
variant="surface" da a la tabla borde y fondo, y "ghost" deja solo las líneas. size es el relleno de las celdas.
PartePropiedadValoresUso
rootvariant"surface" "ghost"Tabla con borde y fondo, o solo con líneas
rootsize"1" "2" "3"Relleno de las celdas y tamaño del texto
rootalign · summary"left" "center" "right" · cadenaAlineación de la tabla, resumen de la tabla (atributos HTML)
rowalign"start" "center" "end" "baseline"Alineación vertical de las celdas de la fila
column_header_cell · cell · row_header_celljustify"start" "center" "end"Alineación horizontal del contenido de la celda
Las tres celdascol_span · row_spanEnteroCelda que une varias columnas o filas
Las tres celdasmin_width · max_widthLongitud CSSAncho mínimo y máximo de la columna
cell · row_header_cellp px py pt pr pb pl"0" ~ "9"Relleno interior de la celda (escala de espaciado del tema)
column_header_cell · row_header_cellscope"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.

Unir celdas y alineación vertical

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.

DayMorningAfternoon
MonWorkshop
TueReviewDemo
WedQ&A
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%",
)

Mostrar datos del State: foreach

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).

Full nameEmailGroup
Danilo Sousadanilo@example.comDeveloper
Zahra Ambessazahra@example.comAdmin
Jasper Eriksjasper@example.comDeveloper
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%",
)

Ordenar y buscar

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.

Full nameEmailGroup
Danilo Sousadanilo@example.comDeveloper
Zahra Ambessazahra@example.comAdmin
Jasper Erikszjasper@example.comB-Developer
Alice Brownalice@example.comAdmin
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.

Paginación

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

NameEmailPhone
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.

Descargas

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.

NameEmailPhone
John Doejohn@example.com555-1234
Jane Smithjane@example.com555-5678
Bob Johnsonbob@example.com555-9012
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",
)

Completar el pedido

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".

Your Team

Invite and manage your team members

Danilo Sousadanilo@example.comDeveloper
Zahra Ambessazahra@example.comAdmin
Jasper Erikssonjasper@example.comDeveloper
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.

← Anterior

Multimedia · Video

Siguiente →

Tablas y cuadrículas de datos · Data Table

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