Table
Display rows of data in columns.
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.
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.
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_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.
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())An element expression with window: &mut Window and cx: &mut App in scope.
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)
}))An element expression with window: &mut Window and cx: &mut App in scope.
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()
})Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
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.
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)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>.
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,
),
])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>.
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).
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>.
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).
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.
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()
})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>.
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()
}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.
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.))`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.
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.)Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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()
}An element expression with window: &mut Window and cx: &mut App in scope.
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(),
])An empty collection renders the empty message in place of rows. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::Table;
Table::new(vec!["Name".into(), "Role".into()])
.id("table-empty-state")
.empty_state("No results found")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.
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)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>.
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).
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>.
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).
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.
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.
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()
}fn row(children: Vec<AnyElement>) -> AnyElement {
gpui::div()
.flex()
.flex_wrap()
.items_center()
.justify_center()
.gap(px(12.))
.children(children)
.into_any_element()
}/// 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
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
| Method | Values | Description |
|---|---|---|
variant(TableVariant) | TableVariant::Primary | TableVariant::Secondary | Primary draws the gray tray and surface card; secondary is flat. |
// 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
| Builder | Values | Default | Description |
|---|---|---|---|
variant( | TableVariant:: | TableVariant:: | Primary draws the gray tray and surface card; secondary is flat. |
new(Partial | AnyElement | — | Compound ScrollContainer, Content, Footer and related parts; takes columns up front and builders for the rest. |
selection_mode( | SelectionMode:: | SelectionMode:: | Adds the selection column and row-activation behaviour when not none. |
selected_keys(Partial | Selection | — | Controlled selected keys as shared strings rather than RAC's Set/"all" union. |
default_selected_keys( | Selection | — | Uncontrolled selection seed; later changes stay in the table's keyed state until selectedKeys is supplied. |
on_selection_change( | Fn( | — | Reports the whole selection after a row, checkbox or select-all change. |
sort_descriptor( | SortDescriptor | — | Current sort column and direction, shown in the header. The table does not reorder its own rows. |
on_sort_change( | Fn( | — | Sortable header activation; the caller rebuilds rows from the descriptor. |
disabled_keys( | iterable of keys | — | Pinned RAC inherited keys excluded from selection, actions and the roving tab stops. |
on_row_click(Partial | Fn( | — | Pinned RAC row action; reports the visible row index rather than the collection key. |
expanded_keys( | iterable of keys | — | Controlled parent keys whose nested rows are shown. |
on_expanded_change( | Fn( | — | Reports the expanded set after a tree chevron is pressed. |
tree_column(Partial | Key | — | Column that carries the expand chevron and indent; takes a column index rather than a column key. |
selection_behavior( | SelectionBehavior:: | SelectionBehavior:: | Controls plain multi-selection activation: toggle preserves the set, while replace selects only the activated row and selects on keyboard focus. |
disallow_empty_selection( | true | | false | Keeps the final selected row when a toggle or Escape would otherwise clear the collection. |
columns(Partial | T[] | — | Dynamic column data for the render-prop pattern; takes configured TableColumn values. |
column(Partial | AnyElement | | — | Static columns or a render prop; appends configured columns. |
new(Partial | string | — | Column identifier; keys sorting and labels by the constructor text rather than a separate id. |
allows_sorting( | true | | false | Makes the header a sort control. |
is_row_header(Partial | true | | false | Marks the identifying column; without an accessibility tree the effect is medium-weight cell text. |
default_width(Partial | string | | — | Starting width for a resizable column. CSS length strings are not accepted; takes pixels. |
min_width( | number | 75 | Pinned RAC resize floor; also applies this 75px default when resizing. |
max_width( | number | — | Pinned RAC inherited ceiling for a resizable column. |
allows_resizing( | true | | false | Pinned RAC inherited flag that draws a drag handle on the column's trailing edge. |
width(Partial | ColumnSize | | — | Caller-owned controlled width in pixels; CSS length and fractional strings are not accepted. |
row(Partial | T[] | — | 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(Partial | Fn( | — | Content shown when there are no rows; takes an element rather than a render-prop that receives TableBodyRenderProps. |
key(Partial | string | | — | Row identifier used for selection; numeric RAC keys are represented as shared strings, defaulting to the row index. |
new(Partial | AnyElement | — | Row cells; takes opaque elements rather than Table.Cell parts, so cell-level RAC props are not independently addressable. |
text_value( | string | — | Pinned RAC typeahead string; cells are opaque, so rows without this value do not participate. |
children(Partial | true | | — | Whether nested rows exist, even if not loaded; infers this from nested TableRow children. |
indicator( | 'ascending' | | — | Current direction, handed to the sort-indicator closure rather than accepted as a header child prop. |
show_indicator( | true | | true | Whether a sorted column draws its direction chevron. |
footer( | AnyElement | — | Footer content such as pagination. |
on_resize_start(Partial | Fn( | — | Reports known pixel widths when pointer or keyboard resizing begins. |
on_resize(Partial | Fn( | — | Reports each proposed pixel width map; feed values back through controlled TableColumn.width. |
on_resize_end(Partial | Fn( | — | Reports the final known pixel widths after pointer or keyboard resizing ends. |
is_pending( | true | | false | Shows the load-more sentinel content; spells the loading flag as is_pending. |
on_load_more( | Fn( | — | Called when the sentinel intersects the scroll viewport. |
scroll_offset( | number | 1 | Pinned RAC inherited viewport-height margin before the sentinel counts as visible. |
virtual_rows(Partial | T[] | — | 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( | number | | 48 | Fixed 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( | number | | — | Estimated height for variable virtual rows; selects a measured VirtualList instead of the uniform one. |
loader_height( | number | | 48 | Fixed height of the load-more row. |
gap( | number | | 0 | Space between rows; leaves this at 0. |
padding( | number | | 0 | Padding around the list; leaves this at 0. |
Not ported30 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Table.className | For the root container. |
Table.ScrollContainer.className | For the horizontal scroll wrapper. |
Table.Content.aria-label | Accessible name for the table; GPUI has no accessibility tree to publish it to. |
Table.Content.disabledBehavior | Pinned RAC disabledKeys scope; disabled rows here are excluded from selection, actions and keyboard stops together. |
Table.Content.dragAndDropHooks | Pinned RAC drag-and-drop hooks; this port does not implement table reordering. |
Table.Content.keyboardNavigationBehavior | Pinned RAC 1.20 cell-vs-widget keyboard mode; interactive cell children are not a separate tab cycle here. |
Table.Content.onScroll | Pinned RAC scroll callback on the resizable container; the port's scroller is internal. |
Table.Content.className | For the table element. |
Table.Header.className | For the header row. |
Table.Column.className | For the column header. |
Table.Column.textValue | Pinned RAC accessibility string for the column; unused without an accessibility tree. |
Table.Column.focusMode | Pinned RAC header focus target; focuses the sortable header or resizer itself. |
Table.Column.allowsArrowNavigation | Pinned RAC 1.20 arrow navigation inside a tab-mode header; unused without keyboardNavigationBehavior. |
Table.Body.className | For the body. |
Table.Row.className | For the row. |
Table.Cell.className | For a data cell. |
Table.Cell.textValue | Pinned RAC cell typeahead string; typeahead is row-level textValue only. |
Table.Cell.colSpan | Pinned RAC column span; flex columns cannot merge cells. |
Table.Cell.id | Pinned RAC cell id. |
Table.Cell.focusMode | Pinned RAC cell focus target for interactive descendants. |
Table.Cell.allowsArrowNavigation | Pinned RAC 1.20 arrow navigation inside a tab-mode cell. |
Table.SortableColumnHeader.className | For the sortable header wrapper. |
Table.Footer.className | For the footer. |
Table.ColumnResizer.className | For the resize handle. |
Table.ResizableContainer.className | For the resizable wrapper. |
Table.LoadMore.className | For the sentinel row. |
Table.LoadMoreContent.className | For the loading-indicator container. |
TableLayout.headingHeight | Section-header height; this table has expandable rows, not section rows to size. |
TableLayout.estimatedHeadingHeight | Variable section-header estimate; unused without section rows. |
TableLayout.dropIndicatorThickness | Drop-indicator thickness for a drag API this port does not expose. |
Parts
| Part | Description |
|---|---|
Table | Root tray that owns variant, selection, sort, virtualization and load-more state. |
Table | Horizontal 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. |
TablePartial | Flex 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. |
Table | Header row of column cells, including the optional select-all checkbox. |
TableColumn | Configured header cell with sort, resize and row-header flags. |
Table | Row list, empty state and the primary-variant surface card. |
TableRow | One body row with optional nested children, selection key and typeahead text. |
TableRowPartial | Data cells are opaque elements inside TableRow; there is no Cell part to compose or span. |
Table | Sortable header wrapper and direction indicator drawn when allowsSorting is set. |
Table | Optional row under the body, where places pagination. |
TableColumnPartial | Trailing-edge resize handle drawn when allowsResizing is set; not a separately constructed child. |
TablePartial | V3's overflow-auto resize host is not a separate wrapper; resizing hangs off columns and horizontal scroll stays on ScrollContainer. |
Table | Intersection sentinel that fires onLoadMore, including virtual end-of-list detection. |
TablePartial | Built-in spinner and Loading… label; caller children cannot replace that content. |
TablePartial | RAC mixer for static plus dynamic cells; exposes whole-row factories instead. |
States
| State | Builder | Description |
|---|---|---|
Hovered | per-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. |
Selected | cell.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 visible | table_cell_focus_ring | Inset 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 row | disabled_keys | DisabledKeys rows are dimmed and removed from selection, actions and keyboard stops. |
Sortable | allows_sorting | Sortable headers use the interactive cursor, hover fill and keyboard activation. |
Resizing | is_resizing | The handle grows to a full-height accent (or focus) bar while dragged, hovered or keyboard-resizing. |
Empty | empty_state | Caller empty-state content is centered in the body when there are no rows. |
Loading | is_pending | Pending sentinel shows the built-in spinner while onLoadMore can still fire from intersection. |
Not ported3 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Disabled table | Styles a disabled table, but documents no prop that can disable the whole control. |
Dragging | RAC row drag opacity; this port has no table drag-and-drop. |
Drop target | RAC drop-target accent; unused without dragAndDropHooks. |