# Table

A static table whose columns carry the geometry every row obeys, and whose cells can hold a widget rather than only a string.

```slint
import { Table } from "@glint/components/table.slint";
import { TableAlign, TableCellKind } from "@glint/components/table-cell.slint";
import { BadgeVariant } from "@glint/components/badge.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 640px;
    height: 340px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: start;

        Table {
            accessible-label: "Recent invoices";
            caption: "A list of your recent invoices.";
            columns: [
                { title: "Invoice", width: 110px },
                { title: "Status" },
                { title: "Method" },
                { title: "Amount", align: TableAlign.end },
            ];
            rows: [
                { cells: [
                    { text: "INV001" },
                    { kind: TableCellKind.badge, text: "Paid" },
                    { text: "Credit Card" },
                    { text: "$250.00" },
                ] },
                { cells: [
                    { text: "INV002" },
                    { kind: TableCellKind.badge, text: "Pending",
                      badge-variant: BadgeVariant.secondary },
                    { text: "PayPal" },
                    { text: "$150.00" },
                ] },
                { cells: [
                    { text: "INV003" },
                    { kind: TableCellKind.badge, text: "Unpaid",
                      badge-variant: BadgeVariant.destructive },
                    { text: "Bank Transfer" },
                    { text: "$350.00" },
                ] },
            ];
            footer: [{ text: "Total" }, { }, { }, { text: "$750.00" }];
        }
    }
}
```

## Usage

```slint
import { Table } from "@glint/components/table.slint";
import { TableAlign } from "@glint/components/table-cell.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component AppWindow inherits Window {
    width: 520px;
    height: 240px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: start;

        Table {
            accessible-label: "Team seats";
            columns: [
                { title: "Member" },
                { title: "Seats", align: TableAlign.end, width: 80px },
            ];
            rows: [
                { cells: [{ text: "Engineering" }, { text: "24" }] },
                { cells: [{ text: "Design" }, { text: "6" }] },
            ];
        }
    }
}
```

`columns` carries the headings and the geometry every row obeys; each row’s `cells` line up with them by position. Naming the table is the call site’s job — `accessible-label` is what a screen reader announces before the rows — and `caption` is a descriptive title drawn under the table and announced as its description.

`TableRow` describes this component’s rows. `TableColumn`, `TableCell`, `TableAlign` and `TableCellKind` are shared with [DataTable](/docs/components/data-table), while DataTable uses its own `DataTableRow` because its rows also carry selection state:

| Type          | Fields                                                                              |
| ------------- | ----------------------------------------------------------------------------------- |
| `TableColumn` | `title`, `width` (leave it out to share the free space), `align`, `hidden`          |
| `TableRow`    | `cells`, `label` (what a screen reader calls the row; falls back to the first cell) |
| `TableCell`   | `kind`, `text`, `icon`, `badge-variant`, `checked`, `actions`                       |

`TableCellKind` is what a cell draws: `text` (the default), `badge`, `icon`, `checkbox` and `actions`. Every kind reads `text` — it is the value a text cell shows, the label a badge carries, the name an icon is announced by, the label beside a checkbox and the name of an action menu’s button. A kind ignores the fields it has no use for (ADR-0026).

Rows carry no pointer handling of their own and no hover tint, on purpose: a row that lights up under the pointer is promising an interaction this table does not have. Reach for [DataTable](/docs/components/data-table) when the rows themselves must sort, paginate or act.

## Examples

### Column widths and alignment

A column with a `width` keeps it; one without shares the free space equally with the other unsized columns. `align` decides where the cells sit inside that width — numbers read right, labels read left, and a lone control reads centered.

```slint
import { Table } from "@glint/components/table.slint";
import { TableAlign, TableCellKind } from "@glint/components/table-cell.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 640px;
    height: 280px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: start;

        Table {
            accessible-label: "Storage by region";
            columns: [
                { title: "Region", width: 160px },
                { title: "Tier" },
                { title: "Health", align: TableAlign.center, width: 90px },
                { title: "Used", align: TableAlign.end, width: 110px },
            ];
            rows: [
                { cells: [
                    { text: "eu-central-1" },
                    { text: "Standard" },
                    { kind: TableCellKind.icon, text: "Healthy",
                      icon: IconSet.CircleCheck },
                    { text: "1.2 TB" },
                ] },
                { cells: [
                    { text: "us-east-1" },
                    { text: "Infrequent access" },
                    { kind: TableCellKind.icon, text: "Degraded",
                      icon: IconSet.TriangleAlert },
                    { text: "840 GB" },
                ] },
            ];
        }
    }
}
```

### Widget cells

A cell is a `TableCell` rather than a string, so a status badge, an icon, a checkbox or a per-row action menu can stand in one (ADR-0028). A checkbox cell settles its own state in `rows` before `cell-toggled` fires, and an action cell reports through `cell-action` with the entry that was taken.

```slint
import { Table } from "@glint/components/table.slint";
import { TableAlign, TableCellKind } from "@glint/components/table-cell.slint";
import { BadgeVariant } from "@glint/components/badge.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 640px;
    height: 300px;
    background: Tokens.color-background;

    property <string> last: "Nothing taken yet.";

    VerticalLayout {
        padding: 24px;
        spacing: 12px;
        alignment: start;

        Table {
            accessible-label: "Release checklist";
            columns: [
                { title: "Done", align: TableAlign.center, width: 70px },
                { title: "Task" },
                { title: "State", width: 110px },
                { title: "", align: TableAlign.end, width: 60px },
            ];
            rows: [
                { label: "Tag the release", cells: [
                    { kind: TableCellKind.checkbox, checked: true },
                    { text: "Tag the release" },
                    { kind: TableCellKind.badge, text: "Shipped" },
                    { kind: TableCellKind.actions, actions: [
                        { label: "Re-run" }, { label: "Copy link" },
                    ] },
                ] },
                { label: "Publish the notes", cells: [
                    { kind: TableCellKind.checkbox },
                    { text: "Publish the notes" },
                    { kind: TableCellKind.badge, text: "Blocked",
                      badge-variant: BadgeVariant.destructive },
                    { kind: TableCellKind.actions, actions: [
                        { label: "Re-run" }, { label: "Copy link" },
                    ] },
                ] },
            ];
            cell-toggled(row, column, on) => {
                root.last = "Row " + row + " " + (on ? "ticked" : "cleared") + ".";
            }
            cell-action(row, column, entry, child) => {
                root.last = "Action " + entry + " on row " + row + ".";
            }
        }

        Text {
            text: root.last;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
        }
    }
}
```

### A totals line

`footer` draws a row of cells under the body, in the body’s own columns. A footer shorter than the columns leaves the rest blank rather than reading past its own end, so `{ }` is how a column is skipped. Its cells report as row `-1`.

```slint
import { Table } from "@glint/components/table.slint";
import { TableAlign } from "@glint/components/table-cell.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 600px;
    height: 300px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        alignment: start;

        Table {
            accessible-label: "Quarterly spend";
            columns: [
                { title: "Quarter", width: 120px },
                { title: "Team" },
                { title: "Spend", align: TableAlign.end, width: 120px },
            ];
            rows: [
                { cells: [{ text: "Q1" }, { text: "Platform" }, { text: "$12,400" }] },
                { cells: [{ text: "Q2" }, { text: "Platform" }, { text: "$9,850" }] },
                { cells: [{ text: "Q3" }, { text: "Platform" }, { text: "$14,120" }] },
            ];
            footer: [{ text: "Total" }, { }, { text: "$36,370" }];
        }
    }
}
```

### A hidden column

`hidden` keeps a column off screen without reshaping a single row: the cells stay where they are and the column they belong to collapses to nothing. Nothing is left in the accessibility tree either — a hidden column draws no node at all.

```slint
import { Table } from "@glint/components/table.slint";
import { Switch } from "@glint/components/switch.slint";
import { TableAlign } from "@glint/components/table-cell.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 620px;
    height: 300px;
    background: Tokens.color-background;

    in-out property <bool> show-owner: false;

    VerticalLayout {
        padding: 24px;
        spacing: 16px;
        alignment: start;

        Switch {
            label: "Show the Owner column";
            checked <=> root.show-owner;
        }

        Table {
            accessible-label: "Open incidents";
            columns: [
                { title: "Incident", width: 110px },
                { title: "Summary" },
                { title: "Owner", hidden: !root.show-owner, width: 130px },
                { title: "Age", align: TableAlign.end, width: 80px },
            ];
            rows: [
                { cells: [
                    { text: "INC-41" }, { text: "Elevated 5xx on checkout" },
                    { text: "Ada Lovelace" }, { text: "2h" },
                ] },
                { cells: [
                    { text: "INC-42" }, { text: "Search index lagging" },
                    { text: "Grace Hopper" }, { text: "6h" },
                ] },
            ];
        }
    }
}
```

## API Reference

### Properties

| Property        | Type                 | Default              | Description                                                                                                                                                                                                 |
| --------------- | -------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `columns`       | `in [TableColumn]`   | no default           | Columns, left-to-right: their headings, widths and alignment.                                                                                                                                               |
| `rows`          | `in-out [TableRow]`  | no default           | Row data; each row's `cells` line up with `columns` by position. `in-out` because a checkbox cell settles its own state here before `cell-toggled` fires — the model is the state, as it is for a menu row. |
| `footer`        | `in-out [TableCell]` | no default           | Totals line under the body, in the body's own columns. Empty leaves it off. Its cells report as row -1.                                                                                                     |
| `caption`       | `in string`          | no default           | Descriptive title for the table, drawn under it and announced as its description.                                                                                                                           |
| `actions-label` | `in string`          | `@tr("Row actions")` | Name an action cell's button falls back to when its cell carries no text of its own.                                                                                                                        |

### Callbacks

| Callback                          | Description                                                                                                                                          |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cell-toggled(int, int, bool)`    | A checkbox cell was flipped: its row (-1 in the footer), its column and the state it now holds.                                                      |
| `cell-action(int, int, int, int)` | An action cell fired: its row (-1 in the footer), its column, the entry's index and the submenu leaf's index, or -1 when the entry itself was taken. |

`TableRow`, `TableColumn`, `TableCell` and the `TableAlign` / `TableCellKind` enums are data types rather than components; their fields are in [Usage](#usage) above, and the primitives every table draws them with are on [DataTable’s page](/docs/components/data-table#api-reference).

## Accessibility

- **Table role.** The root carries `accessible-role: table` and publishes `accessible-item-count` as the number of rows. Naming it is the call site’s — set `accessible-label`. `caption` becomes the table’s `accessible-description`, and the caption text under the table is silenced as a node of its own so a screen reader hears it once.
- **Rows.** Slint 1.17 has a `table` role but no row, cell or column-header member, so each row wears `list-item` with its index. A row is announced by its `label`, which falls back to the first cell — the column a table is usually read by.
- **Cells.** A cell that draws no control is a text node announced as its column then its value (“Status: Paid”), carrying the column’s position so assistive technology can navigate across a row as well as down a column. That pairing is as close to header association as the platform gets.
- **Widget cells speak for themselves.** A checkbox cell announces itself a checkbox and an action cell a button, each carrying the column’s position the same way. A box with nothing written beside it is named by its column; `actions-label` names an action button whose cell carries no text.
- **Hidden columns leave no trace.** A hidden column draws nothing at all rather than a collapsed node assistive technology would still find.
- **No row affordance.** Rows take no pointer events and draw no hover tint, so nothing invites a click the table does not answer. Only a widget cell takes the pointer, and it takes it the way any control does.
