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
Tables and data grids

Data Editor

rx.data_editorrx.data_editor_theme

Official docs

Workshop order

Make the café's inventory sheet editable in place, like a spreadsheet, and recalculate the total stock value after every edit.

rx.data_editor is a data grid where you select cells and edit them, like a spreadsheet. It is rendered with Glide Data Grid, so it stays fast even with a great many rows and columns. For a read-only table, Data Table is enough; reach for this component when users need to change values.

columns is a list with one dict per column.

  • title: the column title.
  • id: the column's id. If missing, it is the title in lowercase.
  • width: column width (px).
  • type: the cell type. Use "str" (default), "int", "float", "bool" or "image".
  • group: draws one extra group title above the columns that share the same value.

data is a list with one list per row. Each value must match its column's type.

rx.data_editor(
    columns=[
        {"title": "Code", "type": "str"},
        {"title": "Value", "type": "int"},
        {"title": "Activated", "type": "bool"},
    ],
    data=[
        ["A", 1, True],
        ["B", 2, False],
        ["C", 3, False],
        ["D", 4, True],
        ["E", 5, True],
        ["F", 6, False],
    ],
)

Try the props

Click the grid to select cells, rows and columns. Double-click a cell to open the editor (this lab doesn't save values, so an edited value soon snaps back).

Try the props

row_markers

column_select

row_select

range_select

vertical_border

show_search

rx.data_editor(
    columns=[
        {"title": "Code", "type": "str"},
        {"title": "Value", "type": "int"},
        {"title": "Activated", "type": "bool"},
    ],
    data=[["A", 1, True], ["B", 2, False], ["C", 3, False], ["D", 4, True]],
    row_markers="number",
    column_select="single",
    row_select="multi",
    range_select="rect",
    vertical_border=True,
    show_search=False,
    height="220px",
)
row_markers is the marker in front of each row (number, checkbox), *_select sets what can be selected, and show_search is the search box at the top.
PropValuesUse
columns · datalistColumn settings, row data
rowsintNumber of rows. Defaults to the length of data
row_height · header_height · group_header_heightint (px)Height of rows, column titles and group titles
row_markers"none" "number" "checkbox" "both" "checkbox-visible" "clickable-number"Marker in front of each row
row_marker_start_index · row_marker_widthintFirst row number, marker column width
freeze_columnsintNumber of columns pinned on the left during horizontal scrolling
min_column_width · max_column_width · max_column_auto_widthint (px)Limits on column width
column_select · row_select"none" "single" "multi"Selecting columns and rows
range_select"none" "cell" "rect" "multi-cell" "multi-rect"Selecting cell ranges
*_selection_blending · row_selection_mode · span_range_behaviorstringFine details, such as whether different kinds of selection can coexist
smooth_scroll_x · smooth_scroll_yTrue / FalseSmooth scrolling
vertical_border · draw_focus_ring · fixed_shadow_x · fixed_shadow_yTrue / FalseVertical lines, focus ring, shadow on the pinned area
fill_handleTrue / FalseHandle on the cell corner that fills values by dragging
get_cells_for_selectionTrue / FalseAllow copying the selected cells (default True)
overscroll_x · overscroll_y · scroll_offset_x · scroll_offset_yintExtra scroll room past the end, initial scroll position
prevent_diagonal_scrolling · show_searchTrue / FalseBlock diagonal scrolling, search box
themerx.data_editor_theme(...) or dictColors and fonts
grid_selectionselection dictThe current selection. If you use on_grid_selection_change, always wire it up too
EventReceivesWhen
on_cell_clicked · on_cell_activated · on_cell_context_menu[col, row]A cell is clicked, activated with Enter or a double-click, or right-clicked
on_cell_edited[col, row], new cell dictA cell value is edited (the new value is in dict["data"])
on_finished_editingnew cell dict, move directionThe editor closes
on_header_clicked · on_header_context_menu[col, row]A column title is clicked or right-clicked
on_header_menu_clickcolumn, positionThe menu button on a column title is clicked
on_group_header_clicked · on_group_header_context_menu · on_group_header_renamed-A group title is clicked, right-clicked or renamed
on_item_hovered[col, row]The pointer moves over a cell
on_grid_selection_change · on_selection_clearedselection dict · -The selection changes or is cleared
on_deleteselection dictDelete is pressed on the selected cells
on_row_appended · on_column_resize · on_search_close- · column, width · -A row is appended, a column is resized, the search box closes

Image cells

A column with type="image" shows image URLs as small thumbnails. A value can be a single URL or a list of URLs holding several images. Increase row_height so the images are easy to see. Clicking an image cell opens a larger view.

rx.data_editor(
    columns=[
        {"title": "Preview", "type": "image", "width": 120},
        {"title": "Name", "type": "str"},
    ],
    data=[
        ["https://picsum.photos/id/237/160/100", "Puppy"],
        ["https://picsum.photos/id/1025/160/100", "Pug"],
        [["https://picsum.photos/id/1084/160/100", "https://picsum.photos/id/1074/160/100"], "Two animals"],
    ],
    row_height=90,
    height="320px",
)

Clicking a cell: on_cell_clicked

on_cell_clicked passes the position of the clicked cell as [col, row] (both starting at 0). The column settings below include group: "Data", so the five columns other than Title get an extra group title, "Data", above them.

Cell clicked:

from typing import Any


class DataEditorClickState(rx.State):
    clicked_data: str = "Cell clicked: "
    cols: list[dict[str, Any]] = [
        {"title": "Title", "type": "str"},
        {"title": "Name", "type": "str", "group": "Data", "width": 220},
        {"title": "Birth", "type": "str", "group": "Data", "width": 150},
        {"title": "Human", "type": "bool", "group": "Data", "width": 80},
        {"title": "House", "type": "str", "group": "Data"},
        {"title": "Patronus", "type": "str", "group": "Data"},
    ]
    data: list[list[Any]] = [
        ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "Stag"],
        ["2", "Ronald Bilius Weasley", "1 March 1980", True, "Gryffindor", "Jack Russell terrier"],
        ["3", "Hermione Jean Granger", "19 September 1979", True, "Gryffindor", "Otter"],
        ["4", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "None"],
    ]

    @rx.event
    def click_cell(self, pos: tuple[int, int]):
        col, row = pos
        self.clicked_data = f"Cell clicked: column {col}, row {row}"


rx.vstack(
    rx.heading(DataEditorClickState.clicked_data, size="3"),
    rx.data_editor(
        columns=DataEditorClickState.cols,
        data=DataEditorClickState.data,
        on_cell_clicked=DataEditorClickState.click_cell,
        height="220px",
    ),
    width="100%",
)

Saving edits: on_cell_edited

Data Editor only lets cells be edited; it does not save the edited values by itself. The on_cell_edited handler receives the position and the new cell info, and has to update data in State for the change to stay on screen. The new value is in new_value["data"]. Double-click a cell to edit it, or click an Activated checkbox.

The official docs don't include an example of this event, but you can't build an editable grid without it.

Last edit: nothing yet

from typing import Any


class DataEditorEditState(rx.State):
    cols: list[dict[str, Any]] = [
        {"title": "Code", "type": "str"},
        {"title": "Value", "type": "int"},
        {"title": "Activated", "type": "bool"},
    ]
    data: list[list[Any]] = [["A", 1, True], ["B", 2, False], ["C", 3, False]]
    last_edit: str = "nothing yet"

    @rx.event
    def edit_cell(self, pos: tuple[int, int], new_value: dict[str, Any]):
        col, row = pos
        rows = [list(r) for r in self.data]
        rows[row][col] = new_value.get("data")
        self.data = rows
        self.last_edit = f"row {row}, column {col} -> {new_value.get('data')}"

    @rx.event
    def add_row(self):
        self.data = self.data + [["New", 0, False]]


rx.vstack(
    rx.data_editor(
        columns=DataEditorEditState.cols,
        data=DataEditorEditState.data,
        on_cell_edited=DataEditorEditState.edit_cell,
        row_markers="number",
        height="200px",
    ),
    rx.hstack(
        rx.button("Add row", size="1", on_click=DataEditorEditState.add_row),
        rx.text("Last edit: ", DataEditorEditState.last_edit, size="2"),
        align="center",
    ),
    width="100%",
)

Styling: theme

Pass colors and fonts to theme, either as keywords to rx.data_editor_theme(...) (in the official docs, from reflex.components.datadisplay.dataeditor import DataEditorTheme) or as a plain dict. Keys work both in snake case, like accent_color, and in camel case, like accentColor. The example below adds row_height=50, smooth scrolling and single-column selection to a dark theme.

dataeditor_dark_theme = {
    "accent_color": "#8c96ff",
    "accent_light": "rgba(202, 206, 255, 0.253)",
    "text_dark": "#ffffff",
    "text_medium": "#b8b8b8",
    "text_light": "#a0a0a0",
    "text_bubble": "#ffffff",
    "bg_icon_header": "#b8b8b8",
    "fg_icon_header": "#000000",
    "text_header": "#a1a1a1",
    "text_header_selected": "#000000",
    "bg_cell": "#16161b",
    "bg_cell_medium": "#202027",
    "bg_header": "#212121",
    "bg_header_has_focus": "#474747",
    "bg_header_hovered": "#404040",
    "bg_bubble": "#212121",
    "bg_bubble_selected": "#000000",
    "bg_search_result": "#423c24",
    "border_color": "rgba(225,225,225,0.2)",
    "drilldown_border": "rgba(225,225,225,0.4)",
    "link_color": "#4F5DFF",
    "header_font_style": "bold 14px",
    "base_font_style": "13px",
}


rx.data_editor(
    columns=[
        {"title": "Code", "type": "str", "width": 120},
        {"title": "Value", "type": "int", "width": 120},
        {"title": "Activated", "type": "bool", "width": 120},
    ],
    data=[["A", 1, True], ["B", 2, False], ["C", 3, False], ["D", 4, True], ["E", 5, True]],
    row_height=50,
    smooth_scroll_x=True,
    smooth_scroll_y=True,
    column_select="single",
    theme=rx.data_editor_theme(**dataeditor_dark_theme),
    height="260px",
)

Filling the order

Save the inventory sheet with on_cell_edited, and make the total stock value and the reorder count computed vars. Edit a quantity or unit price and the badges below change right away. The item column is pinned with freeze_columns=1.

Stock value: $263.60To reorder: 1
from typing import Any


class DataEditorStockState(rx.State):
    cols: list[dict[str, Any]] = [
        {"title": "Item", "type": "str", "width": 160},
        {"title": "Qty", "type": "int", "width": 80},
        {"title": "Unit price", "type": "float", "width": 110},
        {"title": "Reorder", "type": "bool", "width": 90},
    ]
    data: list[list[Any]] = [
        ["Coffee beans", 12, 18.5, False],
        ["Paper cups", 300, 0.1, False],
        ["Oat milk", 4, 2.9, True],
    ]

    @rx.event
    def edit_cell(self, pos: tuple[int, int], new_value: dict[str, Any]):
        col, row = pos
        rows = [list(r) for r in self.data]
        rows[row][col] = new_value.get("data")
        self.data = rows

    @rx.var
    def total(self) -> str:
        value = sum(float(r[1] or 0) * float(r[2] or 0) for r in self.data)
        return f"${value:,.2f}"

    @rx.var
    def reorder_count(self) -> int:
        return sum(1 for r in self.data if r[3])


rx.vstack(
    rx.data_editor(
        columns=DataEditorStockState.cols,
        data=DataEditorStockState.data,
        on_cell_edited=DataEditorStockState.edit_cell,
        row_markers="number",
        freeze_columns=1,
        height="180px",
    ),
    rx.hstack(
        rx.badge("Stock value: ", DataEditorStockState.total, size="2"),
        rx.badge("To reorder: ", DataEditorStockState.reorder_count, color_scheme="orange", size="2"),
    ),
    width="100%",
)

Workshop notes

  • columns is a list of {title, type, width, id, group} dicts; data is a list of rows. Values must match the column's type.

  • type: str · int · float · bool · image.

  • Edits only stick if on_cell_edited((col, row), new cell) updates data in State.

  • Selection range: column_select · row_select · range_select; row markers: row_markers.

  • theme=rx.data_editor_theme(...) or a dict; size is set with height and row_height.

← Previous

Tables and data grids · Data Table

Next →

Charts · Area Chart

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