Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Data display

Table

Display rows of data in columns.

Rust
use herogpui::components::table::Table;

Table on docs.rsSource at v0.14.0

Usage

Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.

Usage

Headers use 12px text with 16px lines; cells use 14px text with 20px lines in both ordinary and virtual rows. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Chip, ChipLabel, Color, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

build("tbl-usage")

Row Hover

`row_hover_bg` names the fill a hovered unselected interactive row takes; a selected row keeps its selection fill. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Chip, ChipLabel, Color, SelectionMode, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

build("tbl-row-hover")
    .selection_mode(SelectionMode::Single)
    .row_hover_bg(cx.colors().accent.soft())

Variants

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Chip, ChipLabel, Color, Size, Table, TableRow, TableVariant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(TableVariant::ALL
        .iter()
        .map(|v| {
            build(match v {
                TableVariant::Primary => "tbl-variant-primary",
                TableVariant::Secondary => "tbl-variant-secondary",
            })
            .variant(*v)
        }))

Custom sort indicator

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{SortDescriptor, SortDirection, Table, TableColumn};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

Table::new(vec![])
    .id("table-custom-sort-indicator")
    .column(TableColumn::new("Name").allows_sorting(true))
    .column("Role")
    .row(vec![
        gpui::div().child("Tony Reichert").into_any_element(),
        gpui::div().child("CEO").into_any_element(),
    ])
    .row(vec![
        gpui::div().child("Zoey Lang").into_any_element(),
        gpui::div().child("Tech Lead").into_any_element(),
    ])
    .sort_descriptor(SortDescriptor::new(
        "Name",
        SortDirection::Ascending,
    ))
    .indicator(|dir| {
        gpui::div()
            .text_size(px(11.))
            .child(match dir {
                SortDirection::Ascending => "▲",
                SortDirection::Descending => "▼",
            })
            .into_any_element()
    })

Selection

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Chip, ChipLabel, Color, SelectionMode, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![
        build("tbl-selection")
            .selection_mode(SelectionMode::Multiple)
            .selected_keys(self.table_selection.clone())
            .on_selection_change(cx.listener(
                |this, keys: &[SharedString], _, cx| {
                    this.table_selection = keys.to_vec();
                    cx.notify();
                },
            ))
            .into_any_element(),
        para(&format!("{} selected", self.table_selection.len()), cx),
    ])

Also calls para (included when you copy).

Selection Behavior

Replace mode selects the focused row while navigating and makes a plain activation the sole selection; the seeded row cannot be cleared when `disallow_empty_selection` is enabled. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Chip, ChipLabel, Color, SelectionBehavior, SelectionMode, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

build("tbl-selection-replace")
    .selection_mode(SelectionMode::Multiple)
    .selection_behavior(SelectionBehavior::Replace)
    .default_selected_keys(["0"])
    .disallow_empty_selection(true)

Sorting

PageUp moves from the body to the first header — from the top of a virtual body; mid-body it pages by viewport. Enter sorts a sortable header; Down or PageDown returns to the first or last enabled row. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, SortDescriptor, SortDirection, Table, TableColumn};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, App, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![
        {
            let mut rows = [
                ["Tony Reichert", "CEO", "Active"],
                ["Zoey Lang", "Tech Lead", "Paused"],
            ];
            if let Some(sort) = &self.table_sort {
                let column = usize::from(sort.column.as_ref() == "Role");
                rows.sort_by(|a, b| match sort.direction {
                    SortDirection::Ascending => a[column].cmp(b[column]),
                    SortDirection::Descending => b[column].cmp(a[column]),
                });
            }
            let mut sortable = Table::new(vec![])
                .id("table-sorting")
                .column(
                    TableColumn::new("Name")
                        .allows_sorting(true)
                        .is_row_header(true),
                )
                .column(TableColumn::new("Role").allows_sorting(true))
                .column("Status")
                .on_sort_change(cx.listener(
                    |this, d: &SortDescriptor, _, cx| {
                        this.table_sort = Some(d.clone());
                        cx.notify();
                    },
                ));
            for row in rows {
                sortable = sortable.row(row.into_iter().map(|text| {
                    gpui::div().child(text).into_any_element()
                }).collect());
            }
            if let Some(d) = self.table_sort.clone() {
                sortable = sortable.sort_descriptor(d);
            }
            sortable.into_any_element()
        },
        para(
            &match &self.table_sort {
                Some(d) => format!("Sorted by {} {:?}", d.column, d.direction),
                None => "Unsorted".to_owned(),
            },
            cx,
        ),
    ])

Also calls row, para (included when you copy).

Virtualization

Cells are built elements, which cannot be handed out twice, so a virtual table takes a row factory and asks for the rows the viewport shows — one thousand of them, forty pixels each. The fixed-row body caps at `max_h`, shrinks below it in a bounded parent, and PageUp/PageDown move by the visible viewport, including after resize, while skipping disabled stops. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Table, TableColumn, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![
        Table::new(vec![])
            .id("table-virtualization")
            .column(TableColumn::new("Name").is_row_header(true))
            .column("Email")
            .row_height(px(40.))
            .max_h(px(320.))
            .virtual_rows(
                1000,
                "virtual-users",
                |i| i.to_string().into(),
                |i| {
                    let (name, email) = virtual_user(i);
                    TableRow::new(vec![
                        gpui::div().child(name).into_any_element(),
                        gpui::div().child(email).into_any_element(),
                    ])
                },
            )
            .virtual_text_value(|i| virtual_user(i).0.into())
            .into_any_element(),
        para(
            "`estimated_row_height` virtualizes rows that differ: gpui's \
                                 `list` measures each one it builds, and `loader_height` \
                                 fixes the load-more row underneath.",
            cx,
        ),
        Table::new(vec![])
            .id("table-virtual-var")
            .column(TableColumn::new("Name").is_row_header(true))
            .column("Email")
            .estimated_row_height(px(44.))
            .loader_height(px(44.))
            .max_h(px(320.))
            .is_pending(true)
            .virtual_rows(
                1000,
                "virtual-variable-users",
                |i| i.to_string().into(),
                |i| {
                    let (name, email) = virtual_user(i);
                    let mut cells = vec![gpui::div()
                        .flex()
                        .flex_col()
                        .child(name)
                        .when(i % 3 == 0, |el| {
                            el.child(
                                gpui::div()
                                    .text_size(px(12.))
                                    .child("Signed up this week"),
                            )
                        })
                        .into_any_element()];
                    cells.push(gpui::div().child(email).into_any_element());
                    TableRow::new(cells)
                },
            )
            .virtual_text_value(|i| virtual_user(i).0.into())
            .into_any_element(),
    ])

Also calls virtual_user, para (included when you copy).

Column Resizing

Drag a trailing-edge divider, or focus it with Tab, press Enter, and use the arrow keys. This example feeds onResize values back as controlled column widths and reports completion through onResizeEnd. Scroll horizontally to reach wide columns; vertical wheel input does not shift them sideways. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Table, TableColumn};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

{
    let resize_name = self.demo_value("tbl-resize-name", 220.);
    let resize_role = self.demo_value("tbl-resize-role", 180.);
    let resize_status = self.demo_text_value("tbl-resize-status");
    gpui::div()
        .flex()
        .flex_col()
        .w_full()
        .gap(px(12.))
        .children(vec![
                para(
                    &format!(
                        "{} Name: {:.0}px · Role: {:.0}px",
                        if resize_status.is_empty() {
                            "Ready."
                        } else {
                            resize_status.as_str()
                        },
                        resize_name,
                        resize_role,
                    ),
                    cx,
                ),
                Table::new(vec![])
                    .id("table-column-resizing")
                    .column(
                        TableColumn::new("Name")
                            .allows_resizing(true)
                            .width(px(resize_name))
                            .min_width(px(120.)),
                    )
                    .column(
                        TableColumn::new("Role")
                            .allows_resizing(true)
                            .width(px(resize_role)),
                    )
                    .column(TableColumn::new("Status").default_width(px(140.)))
                    .row(vec![
                        gpui::div().child("Tony Reichert").into_any_element(),
                        gpui::div().child("CEO").into_any_element(),
                        gpui::div().child("Active").into_any_element(),
                    ])
                    .row(vec![
                        gpui::div().child("Zoey Lang").into_any_element(),
                        gpui::div().child("Tech Lead").into_any_element(),
                        gpui::div().child("Paused").into_any_element(),
                    ])
                    .on_resize_start(cx.listener(|this, _, _, cx| {
                        this.set_demo_text_value(
                            "tbl-resize-status",
                            "Resizing.".to_owned(),
                        );
                        cx.notify();
                    }))
                    .on_resize(cx.listener(
                        |this, widths: &[(SharedString, gpui::Pixels)], _, cx| {
                            for (column, width) in widths {
                                match column.as_ref() {
                                    "Name" => this.set_demo_value(
                                        "tbl-resize-name",
                                        f32::from(*width),
                                    ),
                                    "Role" => this.set_demo_value(
                                        "tbl-resize-role",
                                        f32::from(*width),
                                    ),
                                    _ => {}
                                }
                            }
                            cx.notify();
                        },
                    ))
                    .on_resize_end(cx.listener(|this, _, _, cx| {
                        this.set_demo_text_value("tbl-resize-status", "Saved.".to_owned());
                        cx.notify();
                    }))
                    .into_any_element(),
            ])
        .into_any_element()
}

Also calls para (included when you copy).

Narrow Width

A table narrower than its columns resolves one shared column track set — the widest cell per column sizes the track the header and every body row sit on — and the scroll container slides that whole grid horizontally instead of letting each row squeeze separately. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Table, TableColumn};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .w(px(320.))
    .child({
        let cell = |text: &str| {
            gpui::div().child(text.to_owned()).into_any_element()
        };
        Table::new(vec![])
            .id("table-narrow-width")
            .column(TableColumn::new("Name").default_width(px(180.)))
            .column(TableColumn::new("Role").default_width(px(150.)))
            .column(
                TableColumn::new("Department").default_width(px(170.)),
            )
            .row(vec![
                cell("Tony Reichert"),
                cell("CEO"),
                cell("Sales"),
            ])
            .row(vec![
                cell("Zoey Lang"),
                cell("Tech Lead"),
                cell("Engineering"),
            ])
            .row(vec![
                cell("Jane Fisher"),
                cell("Designer"),
                cell("Design"),
            ])
            .into_any_element()
    })

Expandable Rows

A row's children are nested under it, and `expandedKeys` decides which parents show theirs. The chevron sits in the tree column; Right expands the focused parent, and Left collapses it or returns the row cursor to its parent. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Table, TableRow};
use herogpui::gpui::SharedString;
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let tbl_expanded = self.demo_selection("tbl-expanded");

{
        let cell = |text: &str| gpui::div().child(text.to_owned());
        Table::new(vec!["Title".into(), "Type".into(), "Modified".into()])
            .id("table-expandable-rows")
            .tree_column(0)
            .expanded_keys(tbl_expanded.iter().cloned())
            .on_expanded_change(cx.listener(
                |this, keys: &[SharedString], _, cx| {
                    this.set_demo_selection("tbl-expanded", keys.to_vec());
                    cx.notify();
                },
            ))
            .tree_row(
                TableRow::new(vec![
                    cell("Documents").into_any_element(),
                    cell("Folder").into_any_element(),
                    cell("8/2/2025").into_any_element(),
                ])
                .key("documents")
                .children(vec![
                    TableRow::new(vec![
                        cell("Reports").into_any_element(),
                        cell("Folder").into_any_element(),
                        cell("8/2/2025").into_any_element(),
                    ])
                    .key("reports")
                    .children(vec![
                        TableRow::new(vec![
                            cell("Weekly Report").into_any_element(),
                            cell("File").into_any_element(),
                            cell("7/10/2025").into_any_element(),
                        ])
                        .key("weekly"),
                        TableRow::new(vec![
                            cell("Budget").into_any_element(),
                            cell("File").into_any_element(),
                            cell("8/20/2025").into_any_element(),
                        ])
                        .key("budget"),
                    ]),
                    TableRow::new(vec![
                        cell("Contract.pdf").into_any_element(),
                        cell("File").into_any_element(),
                        cell("6/1/2025").into_any_element(),
                    ])
                    .key("contract"),
                ]),
            )
            .tree_row(
                TableRow::new(vec![
                    cell("Photos").into_any_element(),
                    cell("Folder").into_any_element(),
                    cell("5/5/2025").into_any_element(),
                ])
                .key("photos")
                .children(vec![
                    TableRow::new(vec![
                        cell("Holiday.jpg").into_any_element(),
                        cell("Image").into_any_element(),
                        cell("5/5/2025").into_any_element(),
                    ])
                    .key("holiday"),
                ]),
            )
            .into_any_element()
    }

Secondary Variant

The flat variant keeps a surface-filled rounded header, tertiary separators on every body cell, and no tray background. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Chip, ChipLabel, Color, Size, Table, TableRow, TableVariant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

build("tbl-secondary-variant")
    .variant(TableVariant::Secondary)
    .radius(px(8.))

Async Loading

`isPending` covers the table while a request is in flight; `onLoadMore` fires when the last row scrolls into view. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Chip, ChipLabel, Color, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

build("tbl-async-loading")
    .is_pending(true)
    .on_load_more(|_, _| {})
    // v3's Async Loading example writes `scrollOffset={0}`.
    .scroll_offset(0.)

Pagination

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Pagination, Size, Table};
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let table_page = self.demo_value("tbl-page", 1.) as usize;

{
    let start = table_page.saturating_sub(1) * 2;
    let people = [
        ("Tony Reichert", "CEO"),
        ("Zoey Lang", "Tech Lead"),
        ("Jane Fisher", "Designer"),
        ("William Howard", "Support"),
        ("Kristen Copper", "Sales Manager"),
        ("Emily Collins", "Marketing"),
    ];
    let mut paged =
        Table::new(vec!["Name".into(), "Role".into()]).id("table-pagination");
    for (name, role) in people.iter().skip(start).take(2) {
        paged = paged.row(vec![
            gpui::div().child(*name).into_any_element(),
            gpui::div().child(*role).into_any_element(),
        ]);
    }
    // v3 puts the pagination in a `Table.Footer`, with a
    // `Pagination.Summary` at its start.
    paged
        .footer(
            Pagination::new("table-pages", table_page, 3)
                .size(Size::Sm)
                .summary(format!(
                    "{} to {} of {} results",
                    start + 1,
                    (start + 2).min(people.len()),
                    people.len()
                ))
                .on_change(cx.listener(|this, p: &usize, _, cx| {
                    this.set_demo_value("tbl-page", *p as f32);
                    cx.notify();
                })),
        )
        .into_any_element()
}

Custom Cells

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Avatar, Chip, ChipLabel, ChipVariant, Color, Size, Table};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

Table::new(vec![
    "Member".into(),
    "Role".into(),
    "Status".into(),
])
.id("table-custom-cells")
.row(vec![
    gpui::div()
        .flex()
        .items_center()
        .gap(px(10.))
        .child(
            Avatar::new("table-tony")
                .name("Tony Reichert")
                .size(Size::Sm)
        )
        .child(
            gpui::div()
                .flex()
                .flex_col()
                .child(gpui::div().child("Tony Reichert"))
                .child(
                    gpui::div()
                        .text_size(px(11.5))
                        .text_color(cx.colors().muted)
                        .child("tony@example.com"),
                ),
        )
        .into_any_element(),
    gpui::div().child("CEO").into_any_element(),
    Chip::new()
        .color(Color::Success)
        .variant(ChipVariant::Soft)
        .size(Size::Sm)
        .child(ChipLabel::new().child("Active"))
        .into_any_element(),
])
.row(vec![
    gpui::div()
        .flex()
        .items_center()
        .gap(px(10.))
        .child(Avatar::new("table-zoey").name("Zoey Lang").size(Size::Sm))
        .child(
            gpui::div()
                .flex()
                .flex_col()
                .child(gpui::div().child("Zoey Lang"))
                .child(
                    gpui::div()
                        .text_size(px(11.5))
                        .text_color(cx.colors().muted)
                        .child("zoey@example.com"),
                ),
        )
        .into_any_element(),
    gpui::div().child("Tech Lead").into_any_element(),
    Chip::new()
        .color(Color::Warning)
        .variant(ChipVariant::Soft)
        .size(Size::Sm)
        .child(ChipLabel::new().child("Paused"))
        .into_any_element(),
])

Empty State

An empty collection renders the empty message in place of rows. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::Table;

Table::new(vec!["Name".into(), "Role".into()])
    .id("table-empty-state")
    .empty_state("No results found")

Loading

The pending skeleton covers the table while a request is in flight. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Chip, ChipLabel, Color, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

build("tbl-loading")
    .is_pending(true)

Column Reordering

HeroGPUI extension, not a example (after gpui-kit's movable columns): `allows_column_reorder` lets a header be dragged onto another column's place, or moved with Alt+Left and Alt+Right while it has the focus (PageUp reaches the headers from the body). `column_order` is controlled here and fed back from `on_column_move`; cells, widths and the tree column move with their column. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Chip, ChipLabel, Color, ColumnMove, Size, Table, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![
        build("tbl-column-reorder")
            .allows_column_reorder(true)
            .column_order(self.table_column_order.clone())
            .on_column_move(cx.listener(|this, m: &ColumnMove, _, cx| {
                this.table_column_order = m.order.clone();
                cx.notify();
            }))
            .into_any_element(),
        para(
            &format!(
                "Order: {}",
                self.table_column_order
                    .iter()
                    .map(|c| ["Name", "Role", "Status"][*c])
                    .collect::<Vec<_>>()
                    .join(", ")
            ),
            cx,
        ),
    ])

Also calls para (included when you copy).

Cell Selection

HeroGPUI extension, not a example (after gpui-kit's cell selection): with `cell_selectable`, a press selects the cell under the pointer, Left and Right move between the cells of a row, Home and End to its ends, and Up and Down to the same column of another row. `selected_cell` is controlled here and fed back from `on_cell_select`. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Chip, ChipLabel, Color, Size, Table, TableCell, TableRow};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

let build = |id: &'static str| {
    Table::new(vec!["Name".into(), "Role".into(), "Status".into()])
        .id(id)
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Tony Reichert").into_any_element(),
                gpui::div().child("CEO").into_any_element(),
                Chip::new()
                    .color(Color::Success)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Active"))
                    .into_any_element(),
            ])
            .text_value("Tony Reichert"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Zoey Lang").into_any_element(),
                gpui::div().child("Tech Lead").into_any_element(),
                Chip::new()
                    .color(Color::Warning)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Paused"))
                    .into_any_element(),
            ])
            .text_value("Zoey Lang"),
        )
        .tree_row(
            TableRow::new(vec![
                gpui::div().child("Jane Fisher").into_any_element(),
                gpui::div().child("Designer").into_any_element(),
                Chip::new()
                    .color(Color::Danger)
                    .size(Size::Sm)
                    .child(ChipLabel::new().child("Vacation"))
                    .into_any_element(),
            ])
            .text_value("Jane Fisher"),
        )
};

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![
        build("tbl-cell-selection")
            .cell_selectable(true)
            .selected_cell(self.table_selected_cell.clone())
            .on_cell_select(cx.listener(|this, cell: &TableCell, _, cx| {
                this.table_selected_cell = Some(cell.clone());
                cx.notify();
            }))
            .into_any_element(),
        para(
            &match &self.table_selected_cell {
                Some(cell) => format!(
                    "Selected: row {}, {}",
                    cell.row,
                    ["Name", "Role", "Status"][cell.column]
                ),
                None => "Selected: nothing".to_owned(),
            },
            cx,
        ),
    ])

Also calls para (included when you copy).

Frozen Columns

HeroGPUI extension, not a example: the Name column and the selection column stay visible while the other columns scroll horizontally. Use a horizontal wheel or trackpad gesture over either part; row selection and keyboard cell navigation still span the whole table. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{SelectionMode, Table, TableColumn};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .w_full()
    .gap(px(12.))
    .children(vec![
        gpui::div()
            .w(px(520.))
            .max_w_full()
            .child({
                let cell = |text: &str| {
                    gpui::div().child(text.to_owned()).into_any_element()
                };
                Table::new(vec![])
                    .id("table-frozen-columns")
                    .column(TableColumn::new("Name").frozen(true).min_width(px(180.)))
                    .column(TableColumn::new("Role").min_width(px(180.)))
                    .column(TableColumn::new("Department").min_width(px(180.)))
                    .column(TableColumn::new("Location").min_width(px(180.)))
                    .column(TableColumn::new("Status").min_width(px(180.)))
                    .selection_mode(SelectionMode::Multiple)
                    .row(vec![cell("Tony Reichert"), cell("CEO"), cell("Leadership"), cell("New York"), cell("Active")])
                    .row(vec![cell("Zoey Lang"), cell("Tech Lead"), cell("Engineering"), cell("London"), cell("Active")])
                    .row(vec![cell("Jane Fisher"), cell("Designer"), cell("Design"), cell("Berlin"), cell("Away")])
                    .into_any_element()
            })
            .into_any_element(),
    ])

Helpers used by these examples

Plain functions over the public API, defined once here and copied with each example that calls them.

para
fn para(text: &str, cx: &App) -> gpui::AnyElement {
    let colors = cx.colors();
    gpui::div()
        .text_size(px(14.5))
        .line_height(px(24.))
        .text_color(colors.foreground)
        .max_w(px(720.))
        .child(text.to_owned())
        .into_any_element()
}
row
fn row(children: Vec<AnyElement>) -> AnyElement {
    gpui::div()
        .flex()
        .flex_wrap()
        .items_center()
        .justify_center()
        .gap(px(12.))
        .children(children)
        .into_any_element()
}
virtual_user
/// The nth of the thousand users v3's virtualization examples list, as
/// `(name, email)`. Same two twenty-name lists, so the rows read the same.
fn virtual_user(i: usize) -> (String, String) {
    pub(super) const FIRST: [&str; 20] = [
        "Emma",
        "Liam",
        "Olivia",
        "Noah",
        "Ava",
        "James",
        "Sophia",
        "Oliver",
        "Isabella",
        "Lucas",
        "Mia",
        "Ethan",
        "Charlotte",
        "Mason",
        "Amelia",
        "Logan",
        "Harper",
        "Alexander",
        "Ella",
        "Benjamin",
    ];
    pub(super) const LAST: [&str; 20] = [
        "Smith",
        "Johnson",
        "Williams",
        "Brown",
        "Jones",
        "Garcia",
        "Miller",
        "Davis",
        "Rodriguez",
        "Martinez",
        "Anderson",
        "Taylor",
        "Thomas",
        "Jackson",
        "White",
        "Harris",
        "Clark",
        "Lewis",
        "Robinson",
        "Walker",
    ];
    let first = FIRST[i % FIRST.len()];
    let last = LAST[(i / FIRST.len()) % LAST.len()];
    (
        format!("{first} {last}"),
        format!("{}.{}@acme.com", first.to_lowercase(), last.to_lowercase()),
    )
}

Anatomy

Rust types

TableTableColumnTableRow

Table is assembled from these builders. The API reference lists each one.

Customization

Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Table looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.

Styling

Table styling
MethodValuesDescription
variant(TableVariant)TableVariant::Primary | TableVariant::SecondaryPrimary draws the gray tray and surface card; secondary is flat.
Rust
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
    el.bg(gpui::rgba(0xffa500ff))             // background
        .text_color(gpui::rgba(0x000000ff))   // text
        .w(gpui::px(13.))                     // size
        .h(gpui::px(12.))
})

API reference

Against HeroUI v3.2.6: 44 entries ported, 37 partial, 33 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Table builders
BuilderValuesDefaultDescription
variant(TableVariant)TableVariant::Primary | TableVariant::SecondaryTableVariant::PrimaryPrimary draws the gray tray and surface card; secondary is flat.
new(columns)PartialAnyElement—Compound ScrollContainer, Content, Footer and related parts; takes columns up front and builders for the rest.
selection_mode(SelectionMode)SelectionMode::None | SelectionMode::Single | SelectionMode::MultipleSelectionMode::NoneAdds the selection column and row-activation behaviour when not none.
selected_keys(keys)PartialSelection—Controlled selected keys as shared strings rather than RAC's Set/"all" union.
default_selected_keys(keys)Selection—Uncontrolled selection seed; later changes stay in the table's keyed state until selectedKeys is supplied.
on_selection_change(callback)Fn(keys: Selection) -> ()—Reports the whole selection after a row, checkbox or select-all change.
sort_descriptor(descriptor)SortDescriptor—Current sort column and direction, shown in the header. The table does not reorder its own rows.
on_sort_change(callback)Fn(descriptor: SortDescriptor) -> ()—Sortable header activation; the caller rebuilds rows from the descriptor.
disabled_keys(keys)iterable of keys—Pinned RAC inherited keys excluded from selection, actions and the roving tab stops.
on_row_click(callback)PartialFn(key: Key) -> ()—Pinned RAC row action; reports the visible row index rather than the collection key.
expanded_keys(keys)iterable of keys—Controlled parent keys whose nested rows are shown.
on_expanded_change(callback)Fn(keys: Key[]) -> ()—Reports the expanded set after a tree chevron is pressed.
tree_column(index)PartialKey—Column that carries the expand chevron and indent; takes a column index rather than a column key.
selection_behavior(SelectionBehavior)SelectionBehavior::Toggle | SelectionBehavior::ReplaceSelectionBehavior::ToggleControls plain multi-selection activation: toggle preserves the set, while replace selects only the activated row and selects on keyboard focus.
disallow_empty_selection(bool)true | falsefalseKeeps the final selected row when a toggle or Escape would otherwise clear the collection.
columns(columns)PartialT[]—Dynamic column data for the render-prop pattern; takes configured TableColumn values.
column(column)PartialAnyElement | Fn(column: T) -> AnyElement—Static columns or a render prop; appends configured columns.
new(label)Partialstring—Column identifier; keys sorting and labels by the constructor text rather than a separate id.
allows_sorting(bool)true | falsefalseMakes the header a sort control.
is_row_header(bool)Partialtrue | falsefalseMarks the identifying column; without an accessibility tree the effect is medium-weight cell text.
default_width(width)Partialstring | number—Starting width for a resizable column. CSS length strings are not accepted; takes pixels.
min_width(width)number75Pinned RAC resize floor; also applies this 75px default when resizing.
max_width(width)number—Pinned RAC inherited ceiling for a resizable column.
allows_resizing(bool)true | falsefalsePinned RAC inherited flag that draws a drag handle on the column's trailing edge.
width(width)PartialColumnSize | null—Caller-owned controlled width in pixels; CSS length and fractional strings are not accepted.
row(cells)PartialT[]—Dynamic row data; static rows are appended one at a time and virtual tables take a factory instead of a generic item list.
empty_state(content)PartialFn() -> AnyElement—Content shown when there are no rows; takes an element rather than a render-prop that receives TableBodyRenderProps.
key(key)Partialstring | number—Row identifier used for selection; numeric RAC keys are represented as shared strings, defaulting to the row index.
new(cells)PartialAnyElement—Row cells; takes opaque elements rather than Table.Cell parts, so cell-level RAC props are not independently addressable.
text_value(value)string—Pinned RAC typeahead string; cells are opaque, so rows without this value do not participate.
children(rows)Partialtrue | false—Whether nested rows exist, even if not loaded; infers this from nested TableRow children.
indicator(render)'ascending' | 'descending'—Current direction, handed to the sort-indicator closure rather than accepted as a header child prop.
show_indicator(bool)true | falsetrueWhether a sorted column draws its direction chevron.
footer(content)AnyElement—Footer content such as pagination.
on_resize_start(callback)PartialFn(widths: Map<Key, ColumnSize>) -> ()—Reports known pixel widths when pointer or keyboard resizing begins.
on_resize(callback)PartialFn(widths: Map<Key, ColumnSize>) -> ()—Reports each proposed pixel width map; feed values back through controlled TableColumn.width.
on_resize_end(callback)PartialFn(widths: Map<Key, ColumnSize>) -> ()—Reports the final known pixel widths after pointer or keyboard resizing ends.
is_pending(bool)true | falsefalseShows the load-more sentinel content; spells the loading flag as is_pending.
on_load_more(callback)Fn() -> ()—Called when the sentinel intersects the scroll viewport.
scroll_offset(offset)number1Pinned RAC inherited viewport-height margin before the sentinel counts as visible.
virtual_rows(count, identity, key, row)PartialT[]—RAC Collection items for mixing static and dynamic cells; the port's analogue is the virtual row factory, not a cell-level collection mixer.
row_height(height)number | undefined48Fixed virtual row height; with virtual_rows this selects a uniform VirtualList, caps at max_h, shrinks below it in a bounded parent, and pages by the visible viewport while skipping disabled stops.
estimated_row_height(height)number | undefined—Estimated height for variable virtual rows; selects a measured VirtualList instead of the uniform one.
loader_height(height)number | undefined48Fixed height of the load-more row.
gap(gap)number | undefined0Space between rows; leaves this at 0.
padding(padding)number | undefined0Padding around the list; leaves this at 0.
Not ported30 HeroUI entries not ported
Table builders not ported
HeroUI v3Why it is not ported
Table.classNameFor the root container.
Table.ScrollContainer.classNameFor the horizontal scroll wrapper.
Table.Content.aria-labelAccessible name for the table; GPUI has no accessibility tree to publish it to.
Table.Content.disabledBehaviorPinned RAC disabledKeys scope; disabled rows here are excluded from selection, actions and keyboard stops together.
Table.Content.dragAndDropHooksPinned RAC drag-and-drop hooks; this port does not implement table reordering.
Table.Content.keyboardNavigationBehaviorPinned RAC 1.20 cell-vs-widget keyboard mode; interactive cell children are not a separate tab cycle here.
Table.Content.onScrollPinned RAC scroll callback on the resizable container; the port's scroller is internal.
Table.Content.classNameFor the table element.
Table.Header.classNameFor the header row.
Table.Column.classNameFor the column header.
Table.Column.textValuePinned RAC accessibility string for the column; unused without an accessibility tree.
Table.Column.focusModePinned RAC header focus target; focuses the sortable header or resizer itself.
Table.Column.allowsArrowNavigationPinned RAC 1.20 arrow navigation inside a tab-mode header; unused without keyboardNavigationBehavior.
Table.Body.classNameFor the body.
Table.Row.classNameFor the row.
Table.Cell.classNameFor a data cell.
Table.Cell.textValuePinned RAC cell typeahead string; typeahead is row-level textValue only.
Table.Cell.colSpanPinned RAC column span; flex columns cannot merge cells.
Table.Cell.idPinned RAC cell id.
Table.Cell.focusModePinned RAC cell focus target for interactive descendants.
Table.Cell.allowsArrowNavigationPinned RAC 1.20 arrow navigation inside a tab-mode cell.
Table.SortableColumnHeader.classNameFor the sortable header wrapper.
Table.Footer.classNameFor the footer.
Table.ColumnResizer.classNameFor the resize handle.
Table.ResizableContainer.classNameFor the resizable wrapper.
Table.LoadMore.classNameFor the sentinel row.
Table.LoadMoreContent.classNameFor the loading-indicator container.
TableLayout.headingHeightSection-header height; this table has expandable rows, not section rows to size.
TableLayout.estimatedHeadingHeightVariable section-header estimate; unused without section rows.
TableLayout.dropIndicatorThicknessDrop-indicator thickness for a drag API this port does not expose.

Parts

Table parts
PartDescription
TableRoot tray that owns variant, selection, sort, virtualization and load-more state.
TableHorizontal overflow wrapper around the content column so a wide table can scroll without stretching the footer. HeroGPUI's frozen-column extension splits the header and each row into a fixed leading part and a scrolling part.
TablePartialFlex column standing in for the table: one measured track triple per column is shared by the header cells, their wrappers and every body row, so a table narrower than its columns overflows the scroll container as one grid. CSS border-separate spacing and the wasm32 measurement path remain out of reach.
TableHeader row of column cells, including the optional select-all checkbox.
TableColumnConfigured header cell with sort, resize and row-header flags.
TableRow list, empty state and the primary-variant surface card.
TableRowOne body row with optional nested children, selection key and typeahead text.
TableRowPartialData cells are opaque elements inside TableRow; there is no Cell part to compose or span.
TableSortable header wrapper and direction indicator drawn when allowsSorting is set.
TableOptional row under the body, where places pagination.
TableColumnPartialTrailing-edge resize handle drawn when allowsResizing is set; not a separately constructed child.
TablePartialV3's overflow-auto resize host is not a separate wrapper; resizing hangs off columns and horizontal scroll stays on ScrollContainer.
TableIntersection sentinel that fires onLoadMore, including virtual end-of-list detection.
TablePartialBuilt-in spinner and Loading… label; caller children cannot replace that content.
TablePartialRAC mixer for static plus dynamic cells; exposes whole-row factories instead.

States

Table states
StateBuilderDescription
Hoveredper-row hover fill applied to each cell (primary or secondary token)Tints each cell with bg-surface/40 (secondary: bg-default/50); uses a per-row hover group so each cell owns and clips the fill.
Selectedcell.bg(surface.background.alpha(0.1))Fills every cell with bg-surface/10; paints the same endpoint on the selection and data cells and omits the hover group for selected rows.
Focus visibletable_cell_focus_ringInset focus ring on the focused row, column header and resizer; row focus is split into clipped top/bottom/edge strips per cell so shared edges stay continuous without a leaking overlay. PageUp enters the first header — from the top of a virtual body, paging by viewport mid-body; Down and PageDown return to the first and last enabled body rows.
Disabled rowdisabled_keysDisabledKeys rows are dimmed and removed from selection, actions and keyboard stops.
Sortableallows_sortingSortable headers use the interactive cursor, hover fill and keyboard activation.
Resizingis_resizingThe handle grows to a full-height accent (or focus) bar while dragged, hovered or keyboard-resizing.
Emptyempty_stateCaller empty-state content is centered in the body when there are no rows.
Loadingis_pendingPending sentinel shows the built-in spinner while onLoadMore can still fire from intersection.
Not ported3 HeroUI entries not ported
Table states not ported
HeroUI v3Why it is not ported
Disabled tableStyles a disabled table, but documents no prop that can disable the whole control.
DraggingRAC row drag opacity; this port has no table drag-and-drop.
Drop targetRAC drop-target accent; unused without dragAndDropHooks.