R

ReflexKR

Reflex-Dokumentation

Die Komponentenwerkstatt: Reflex' 100 Bibliotheksbausteine

·

Kursüberblick

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typografie

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Formulare

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Datenanzeige

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Auf- und Zuklappen

·

Tabs

·

Accordion

·

Segmented Control

Overlays

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamisches Rendern

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Medien

·

Image

·

Audio

·

Video

Tabellen und Datengrids

·

Table

·

Data Table

·

Data Editor

Diagramme

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Diagrammbausteine

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Andere Diagrammbibliotheken

·

Plotly

·

Pyplot

HTML-Elemente

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Weitere Werkzeuge

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

Die Komponentenwerkstatt: Reflex' 100 Bibliotheksbausteine
Tabellen und Datengrids

Table

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

Offizielle Doku

Werkstattauftrag

Baue für die Teamverwaltung eine Tabelle mit allen Teammitgliedern. Jede Zeile enthält Avatar, Namenslink, E-Mail und ein Badge für die Rolle.

rx.table ist eine Tabelle, die du aus Kopfzeile, Zeilen und Zellen selbst zusammensetzt. In jede Zelle passt jede beliebige Komponente, etwa Avatare, Links oder Buttons; damit eignet sie sich für Listen, deren Aussehen du frei bestimmen willst. Brauchst du eine Tabelle, bei der Sortieren, Suchen und Seitenumbruch schon eingebaut sind, bist du mit Data Table womöglich schneller.

  • table.root: die gesamte Tabelle.
  • table.header: der Kopfbereich (<thead>). Darin stehen table.row und table.column_header_cell (Zellen mit Spaltenüberschriften).
  • table.body: der Tabellenkörper (<tbody>). Darin stehen die table.row.
  • table.row: eine Zeile.
  • table.row_header_cell: die erste Zelle, die für die Zeile steht (eine fett dargestellte Zeilenüberschrift).
  • table.cell: eine normale Zelle.
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%",
)

Am besten passt du die Tabelle mit width an den umgebenden Container an. Läuft sie wegen vieler Spalten über, umschließt du sie etwa mit rx.box(rx.table.root(...), overflow_x="auto", width="100%"), damit nur die Tabelle seitlich scrollt und nicht die ganze Seite.

Props ausprobieren

variant und size gehören an table.root, die horizontale Ausrichtung des Zellinhalts justify an die einzelnen Zellen. Zahlenspalten richtet man üblicherweise rechts (end) aus.

Props ausprobieren

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" gibt der Tabelle Rahmen und Hintergrund, "ghost" lässt nur die Linien stehen. size ist der Zellenabstand.
TeilPropWerteVerwendung
rootvariant"surface" "ghost"Tabelle mit Rahmen und Hintergrund bzw. nur mit Linien
rootsize"1" "2" "3"Zellenabstand und Schriftgröße
rootalign · summary"left" "center" "right" · StringAusrichtung der Tabelle, Zusammenfassung (HTML-Attribute)
rowalign"start" "center" "end" "baseline"Vertikale Ausrichtung der Zellen in der Zeile
column_header_cell · cell · row_header_celljustify"start" "center" "end"Horizontale Ausrichtung des Zellinhalts
alle drei Zellartencol_span · row_spanGanzzahlZelle, die mehrere Spalten bzw. Zeilen überspannt
alle drei Zellartenmin_width · max_widthCSS-LängeMinimale und maximale Spaltenbreite
cell · row_header_cellp px py pt pr pb pl"0" ~ "9"Innenabstand der Zelle (Abstandsstufen des Themes)
column_header_cell · row_header_cellscope"col" "row" usw.Bereich, auf den sich die Kopfzelle bezieht (HTML-Attribut)

header und body haben keine eigenen Props. Das align der Zellen ist das alte HTML-Attribut align; für die Radix-Ausrichtung nimmst du justify.

Zellen verbinden und vertikal ausrichten

col_span=2 lässt eine Zelle zwei Spalten einnehmen, row_span=2 zwei Zeilen. Das align einer Zeile bestimmt die vertikale Position ihrer Zellen.

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

Daten aus dem State anzeigen: foreach

Soll sich der Tabelleninhalt ändern können, legst du die Daten in den State und zeichnest die Zeilen mit rx.foreach. Mit einer eigenen Funktion für eine Zeile bleibt der Tabellencode kurz. Statt als Liste kannst du Zeilendaten auch als dict oder dataclass ablegen (bei einer dataclass greifst du per Namen zu, etwa 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%",
)

Sortieren und Suchen

Liegen die Daten im Speicher, sortierst und durchsuchst du sie mit Pythons sorted() und List Comprehensions. Das Original steht in einer reinen Backend-Variable mit führendem Unterstrich wie _people und wird nicht an den Browser geschickt; die anzuzeigende Liste entsteht als berechnete Variable current_people. Dank cache=True wird sie nur neu berechnet, wenn sich Sortierung oder Suchbegriff ändern. An das on_change des Suchfelds hängen wir .debounce(300), damit das Event nicht bei jedem Tastendruck, sondern erst nach einer Tipppause gesendet wird.

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

Liegen die Daten in einer Datenbank, baust du dieselbe Ansicht mit einer SQL-Abfrage. Gekürzt sieht das Beispiel der offiziellen Doku so aus (auf dieser Seite wird es nicht ausgeführt). ilike ist ein Teilstring-Vergleich ohne Rücksicht auf Groß- und Kleinschreibung, und die % vor und nach dem Suchbegriff bedeuten „beliebige Zeichen“.

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

Je größer die Datenmenge, desto eher filtert man auf der Datenbankseite; bei kleinen Datenmengen oder Daten aus einer API ist das Filtern im Speicher einfacher.

Seitenumbruch

Sollen immer nur einige Zeilen auf einmal erscheinen, legst du offset (Anzahl übersprungener Zeilen) und limit (Anzahl angezeigter Zeilen) in den State und schneidest entsprechend viele für users heraus. Mit on_mount an table.root werden die Daten anfangs geladen. Bei einer Datenbank verwendest du statt des Herausschneidens 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%",
)

Stehen mehrere Tabellen mit Seitenumbruch auf einer Seite, braucht jede ihr eigenes offset und limit. Ändern sich Suchbegriff oder Sortierung, setzt du offset auf 0 zurück, damit man nicht auf einer Seite jenseits des Endes der kleineren Ergebnismenge hängen bleibt. Willst du dieselbe Tabelle mehrfach verwenden, kann über rx.ComponentState jede Tabelle ihren eigenen State bekommen.

Herunterladen

Mit rx.download lassen sich die Tabellendaten als Datei herunterladen. Für JSON übergibst du den State-Wert direkt im Frontend; Werte, die keine Strings sind, werden als JSON-Text gespeichert. Für CSV baut der Handler den Text mit dem Modul csv und gibt dann rx.download zurück. In beiden Fällen empfängt der Browser des Nutzers die Datei, auf dem Server wird nichts gespeichert.

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

Auftrag erledigt

Eine Liste der Teammitglieder ohne Kopfzeile, nur mit Tabellenkörper. In der ersten Zelle steht der Avatar, in der Zeilenüberschrift der Namenslink und in der letzten Zelle das Rollen-Badge; align="center" an jeder Zeile gleicht die vertikale Position von Avatar und Text an. Damit die Seite auch auf schmalen Bildschirmen nicht überläuft, steckt die Tabelle in einem Container mit 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",
)

Werkstattnotizen

  • root > header(row > column_header_cell) + body(row > row_header_cell · cell).

  • variant (surface/ghost) und size an root, justify · col_span · row_span an den Zellen, align an der Zeile.

  • Veränderliche Daten: State + rx.foreach. Das Original als _Backend-Variable, die angezeigte Liste als berechnete Variable.

  • Sortieren, Suchen und Seitenumbruch baust du selbst. Brauchst du alles fertig, nimm rx.data_table.

  • Breite Tabellen in einen Container mit overflow_x="auto" packen, damit nur die Tabelle scrollt.

← Zurück

Medien · Video

Weiter →

Tabellen und Datengrids · Data Table

Über neue Lektionen informiert werden

Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.

Erhoben: deine E-Mail-Adresse · Zweck: Hinweise auf neue Lektionen oder den Start dieses Kurses · Speicherdauer: bis du die Löschung verlangst. Keine andere Nutzung, keine Weitergabe. Antworte einfach auf eine Benachrichtigung, dann löschen wir sie sofort.

Fragen & Antworten

Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.

Wird als reiner Text angezeigt, ohne Links oder Formatierung.

R

ReflexKR

Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.

ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee

Hinweis zu Affiliate-Links
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex