Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 and tableGetSelectedRow returns -1. For production lists on platforms without a real impl, use LazyVStack (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.

Next Steps