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
rx.table.rootrx.table.headerrx.table.rowrx.table.column_header_cellrx.table.bodyrx.table.cellrx.table.row_header_cellOffizielle 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.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.
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
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%",
)| Teil | Prop | Werte | Verwendung |
|---|---|---|---|
root | variant | "surface" "ghost" | Tabelle mit Rahmen und Hintergrund bzw. nur mit Linien |
root | size | "1" "2" "3" | Zellenabstand und Schriftgröße |
root | align · summary | "left" "center" "right" · String | Ausrichtung der Tabelle, Zusammenfassung (HTML-Attribute) |
row | align | "start" "center" "end" "baseline" | Vertikale Ausrichtung der Zellen in der Zeile |
column_header_cell · cell · row_header_cell | justify | "start" "center" "end" | Horizontale Ausrichtung des Zellinhalts |
| alle drei Zellarten | col_span · row_span | Ganzzahl | Zelle, die mehrere Spalten bzw. Zeilen überspannt |
| alle drei Zellarten | min_width · max_width | CSS-Länge | Minimale und maximale Spaltenbreite |
cell · row_header_cell | p px py pt pr pb pl | "0" ~ "9" | Innenabstand der Zelle (Abstandsstufen des Themes) |
column_header_cell · row_header_cell | scope | "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.
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.
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%",
)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).
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%",
)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.
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.
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
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.
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.
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",
)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".
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",
)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.
Über neue Lektionen informiert werden
Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Fragen & Antworten
Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.