Table
The Table widget displays tabular data with columns, headers, and row
selection.
Platform support: real implementation lives on macOS (
NSTableView+NSScrollView); the Web target uses an HTML<table>. iOS, Android, Linux/GTK4, Windows, tvOS, visionOS, and watchOS link no-op stubs so cross-platform code compiles everywhere — the table renders nothing andtableGetSelectedRowreturns-1. For production lists on platforms without a real impl, useLazyVStack(see Layout).
Creating a Table
const basicTable = Table(10, 3, (row: number, col: number) => {
return Text(`Row ${row}, Col ${col}`)
})
Table(rowCount, colCount, renderCell) creates a table. The render
callback receives (row, col) and must return a Widget (typically
Text(...)). The runtime resolves the returned handle as the cell
view, which lets cells render images, stacks, or composites — not just
plain strings.
Column Headers
const userTable = Table(users.length, 3, (row: number, col: number) => {
const user = users[row]
if (col === 0) return Text(user.name)
if (col === 1) return Text(user.email)
return Text(user.role)
})
tableSetColumnHeader(userTable, 0, "Name")
tableSetColumnHeader(userTable, 1, "Email")
tableSetColumnHeader(userTable, 2, "Role")
Column Widths
tableSetColumnWidth(userTable, 0, 150) // Name column
tableSetColumnWidth(userTable, 1, 250) // Email column
tableSetColumnWidth(userTable, 2, 100) // Role column
Row Selection
const selectedRow = State(-1)
tableSetOnRowSelect(userTable, (row: number) => {
selectedRow.set(row)
console.log(`Selected row: ${row}`)
})
// Read the currently selected row at any time:
const current = tableGetSelectedRow(userTable)
Dynamic Row Count
Update the number of rows after creation:
tableUpdateRowCount(userTable, users.length)
Complete Example
const selectedName = State("None")
const table = Table(users.length, 3, (row: number, col: number) => {
const user = users[row]
if (col === 0) return Text(user.name)
if (col === 1) return Text(user.email)
return Text(user.role)
})
tableSetColumnHeader(table, 0, "Name")
tableSetColumnHeader(table, 1, "Email")
tableSetColumnHeader(table, 2, "Role")
tableSetColumnWidth(table, 0, 150)
tableSetColumnWidth(table, 1, 250)
tableSetColumnWidth(table, 2, 100)
tableSetOnRowSelect(table, (row: number) => {
selectedName.set(users[row].name)
})
App({
title: "Table Demo",
width: 600,
height: 400,
body: VStack(12, [
table,
Text(`Selected: ${selectedName.value}`),
]),
})
Sort, filter, multi-select (issue #473)
Since v0.5.636 the macOS Table exposes a column-sort callback,
multi-row selection, and a passive filter-text slot the user wires to
their own row-hiding logic.
import {
Table,
tableSetOnSortChange,
tableSetAllowsMultipleSelection,
tableGetSelectedRowsCount,
tableGetSelectedRowAt,
tableSetFilterText,
tableGetFilterText,
} from "perry/ui";
const table = Table(rows.length, cols.length, renderCell);
tableSetAllowsMultipleSelection(table, 1);
tableSetOnSortChange(table, (col, ascending) => {
// Re-sort your data array, then call tableReload(table)
rows.sort((a, b) =>
ascending ? a[col].localeCompare(b[col]) : b[col].localeCompare(a[col]),
);
});
// Multi-select read-back
const n = tableGetSelectedRowsCount(table);
for (let i = 0; i < n; i++) {
console.log("selected:", tableGetSelectedRowAt(table, i));
}
// Passive filter slot — your TS code reads it back and adjusts
// `tableUpdateRowCount(table, filteredRows.length)`.
tableSetFilterText(table, "alice");
console.log(tableGetFilterText(table));
These are real impls on macOS via NSTableView.sortDescriptors and
selectedRowIndexes; other platforms link safe-default stubs.