R

ReflexKR

Reflex Docs

The Component Workshop: Reflex's 100 Library Components

·

Course overview

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typography

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Forms

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Data display

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Disclosure

·

Tabs

·

Accordion

·

Segmented Control

Overlay

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamic rendering

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Media

·

Image

·

Audio

·

Video

Tables and data grids

·

Table

·

Data Table

·

Data Editor

Charts

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Chart parts

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Other chart libraries

·

Plotly

·

Pyplot

HTML elements

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Other tools

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

The Component Workshop: Reflex's 100 Library Components
HTML elements

HTML Tables

rx.el.tablerx.el.captionrx.el.colgrouprx.el.colrx.el.theadrx.el.tbodyrx.el.tfootrx.el.trrx.el.thrx.el.td

Official docs

Workshop order

Turn a café receipt into a table: build it as a plain HTML table with a title, an item / quantity / price header row and a footer row with the total.

These are the HTML table elements for showing data in rows and columns. Inside a table, put rows (tr) in a tbody, and inside each row put header cells (th) and regular cells (td). They have no styling, so you add borders and padding yourself. For a Radix-styled table use rx.table; for a table with sorting and search, use rx.data_table.

NameRole
AdaEngineer
LinusMaintainer
rx.el.table(
    rx.el.tbody(
        rx.el.tr(rx.el.th("Name"), rx.el.th("Role")),
        rx.el.tr(rx.el.td("Ada"), rx.el.td("Engineer")),
        rx.el.tr(rx.el.td("Linus"), rx.el.td("Maintainer")),
    ),
)

Try the props

This site's preflight collapses the gaps between table cells (border_collapse="collapse"). Switch to separate and each cell gets its own detached border.

Try the props

Fruit stock
ItemQty
Apples3
Pears12

caption

caption_side

align

collapse

rx.el.table(
    rx.el.caption("Fruit stock", caption_side="top", padding="4px", font_weight="600"),
    rx.el.thead(
        rx.el.tr(
            rx.el.th("Item", border="1px solid gray", padding="6px"),
            rx.el.th("Qty", border="1px solid gray", padding="6px"),
        ),
    ),
    rx.el.tbody(
        rx.el.tr(
            rx.el.td("Apples", border="1px solid gray", padding="6px"),
            rx.el.td("3", align="right", border="1px solid gray", padding="6px"),
        ),
        rx.el.tr(
            rx.el.td("Pears", border="1px solid gray", padding="6px"),
            rx.el.td("12", align="right", border="1px solid gray", padding="6px"),
        ),
    ),
    border_collapse="collapse",
    border_spacing="4px",
    width="260px",
)
align on td is the horizontal alignment of the text in the cell. caption_side is a CSS property that decides whether the table title goes above or below.
PropValuesDefaultUse
col_span (td, th)integer1Number of cells to merge horizontally
row_span (td, th)integer1Number of cells to merge vertically
scope (th)"col" "row" "colgroup" "rowgroup"noneDirection this header cell covers
headers (td, th)ids of thsnoneNames this cell's header cells explicitly
align (td, th)"left" "center" "right" "justify" "char"leftAlignment within the cell (legacy attribute; CSS text_align works too)
span (col, colgroup)integer1Number of columns one col covers
align summary (table)stringnoneLegacy attributes deprecated in HTML5. Use CSS for alignment and caption for a description

Head, body, foot, caption

thead, tbody and tfoot split the rows into head, body and foot. Always put rows inside one of these three. If you put a tr directly under table, the browser quietly inserts a tbody, which can disagree with the structure React knows about. caption is the table's title and must be the first child of table. A th's scope says whether that header cell covers a column ("col") or a row ("row").

Monthly visitors
MonthVisitors
Jan1,200
Feb1,450
Total2,650
rx.el.table(
    rx.el.caption("Monthly visitors", padding="4px", font_weight="600"),
    rx.el.thead(
        rx.el.tr(rx.el.th("Month", scope="col"), rx.el.th("Visitors", scope="col")),
        background=rx.color("indigo", 3),
    ),
    rx.el.tbody(
        rx.el.tr(rx.el.th("Jan", scope="row"), rx.el.td("1,200")),
        rx.el.tr(rx.el.th("Feb", scope="row"), rx.el.td("1,450")),
    ),
    rx.el.tfoot(
        rx.el.tr(rx.el.th("Total", scope="row"), rx.el.td("2,650", font_weight="bold")),
        border_top="2px solid gray",
    ),
    width="280px",
    text_align="left",
)

Merging cells: col_span and row_span

col_span=2 takes over the cell to its right, and row_span=2 takes over one cell in the row below. Don't put a td where a cell was taken over. In the timetable below, the third row has one cell fewer.

MonTueWed
9 amWorkshopFree
10 amLunch prepReviewFree
11 amFreeDemo
rx.el.table(
    rx.el.thead(rx.el.tr(rx.el.th(""), rx.el.th("Mon"), rx.el.th("Tue"), rx.el.th("Wed"))),
    rx.el.tbody(
        rx.el.tr(rx.el.th("9 am"), rx.el.td("Workshop", col_span=2, background=rx.color("indigo", 4)), rx.el.td("Free")),
        rx.el.tr(rx.el.th("10 am"), rx.el.td("Lunch prep", row_span=2, background=rx.color("amber", 4)), rx.el.td("Review"), rx.el.td("Free")),
        rx.el.tr(rx.el.th("11 am"), rx.el.td("Free"), rx.el.td("Demo")),
    ),
    border="1px solid gray",
    text_align="center",
    width="340px",
)

Styling columns: colgroup and col

Each col inside a colgroup covers one column, or two with span=2. Use them to give a whole column a background color or a width. The CSS you can use on a col is limited to roughly background, width, border and visibility.

PlanPriceSeats
Starter$01
Team$2010
rx.el.table(
    rx.el.colgroup(rx.el.col(), rx.el.col(span=2)),
    rx.el.thead(rx.el.tr(rx.el.th("Plan"), rx.el.th("Price"), rx.el.th("Seats"))),
    rx.el.tbody(
        rx.el.tr(rx.el.td("Starter"), rx.el.td("$0"), rx.el.td("1")),
        rx.el.tr(rx.el.td("Team"), rx.el.td("$20"), rx.el.td("10")),
    ),
    width="300px",
    text_align="left",
    # style the columns from the table: Reflex puts a <style> tag next to a styled
    # element, and the browser moves a <style> found inside <colgroup> out of it
    style={
        "& col:first-child": {"background": rx.color("gray", 3)},
        "& col:last-child": {"background": rx.color("teal", 3)},
    },
)

headers: linking cells to header cells

In complex tables, give each th an id and list those ids in a td's headers. The screen looks the same, but screen readers read it as "Price, $2".

FruitPrice
Kiwi$2
rx.el.table(
    rx.el.tbody(
        rx.el.tr(rx.el.th("Fruit", id="htmltable_fruit"), rx.el.th("Price", id="htmltable_price")),
        rx.el.tr(rx.el.td("Kiwi", headers="htmltable_fruit"), rx.el.td("$2", headers="htmltable_price")),
    ),
    width="200px",
    text_align="left",
)

Building rows from State

When the rows come from a list in the State, build the trs with rx.foreach. Sorting happens in a computed var, and the table just draws the result.

NameScore
Jun95
Mina88
Sora72
class HtmlTableScoreState(rx.State):
    rows: list[dict[str, str]] = [
        {"name": "Mina", "score": "88"},
        {"name": "Jun", "score": "95"},
        {"name": "Sora", "score": "72"},
    ]
    descending: bool = True

    @rx.event
    def flip_order(self):
        self.descending = not self.descending

    @rx.var
    def sorted_rows(self) -> list[dict[str, str]]:
        return sorted(self.rows, key=lambda r: int(r["score"]), reverse=self.descending)


rx.el.div(
    rx.el.button(
        rx.cond(HtmlTableScoreState.descending, "High to low", "Low to high"),
        on_click=HtmlTableScoreState.flip_order,
        padding="4px 12px",
        border="1px solid gray",
        border_radius="6px",
        margin_bottom="8px",
    ),
    rx.el.table(
        rx.el.thead(rx.el.tr(rx.el.th("Name"), rx.el.th("Score"))),
        rx.el.tbody(
            rx.foreach(
                HtmlTableScoreState.sorted_rows,
                lambda row: rx.el.tr(rx.el.td(row["name"]), rx.el.td(row["score"], align="right")),
            ),
        ),
        width="220px",
        text_align="left",
    ),
)

Filling the order

caption is the receipt number, thead the header row and tfoot the total. The total's header cell spans two columns with col_span=2, and the number columns get matching widths from colgroup.

Receipt #1024
ItemQtyPrice
Latte2$9.00
Bagel1$3.50
Total$12.50
rx.el.table(
    rx.el.caption("Receipt #1024", padding="6px", font_weight="bold"),
    rx.el.colgroup(rx.el.col(), rx.el.col(span=2)),
    rx.el.thead(
        rx.el.tr(rx.el.th("Item", scope="col"), rx.el.th("Qty", scope="col"), rx.el.th("Price", scope="col")),
        border_bottom="2px solid gray",
    ),
    rx.el.tbody(
        rx.el.tr(rx.el.td("Latte"), rx.el.td("2", align="right"), rx.el.td("$9.00", align="right")),
        rx.el.tr(rx.el.td("Bagel"), rx.el.td("1", align="right"), rx.el.td("$3.50", align="right")),
    ),
    rx.el.tfoot(
        rx.el.tr(rx.el.th("Total", scope="row", col_span=2), rx.el.td("$12.50", align="right", font_weight="bold")),
        border_top="2px solid gray",
    ),
    width="300px",
    text_align="left",
    font_variant_numeric="tabular-nums",
    style={"& col:last-child": {"width": "70px"}},  # see the colgroup example above
)

Workshop notes

  • table > (caption) > thead, tbody, tfoot > tr > th, td. Always put tr inside tbody or the like.

  • Under preflight, tables have no borders and no padding. Add border and padding yourself.

  • Merge cells with col_span and row_span, and leave out the cells that were taken over.

  • scope on th and headers on td are links for screen readers.

  • When rows come from State, build tr with rx.foreach. For a styled table, use rx.table.

← Previous

HTML elements · HTML Media

Next →

HTML elements · SVG

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

R

ReflexKR

Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.

ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee

Affiliate disclosure
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex