# DataTable

A sortable, selectable, pageable table with hideable columns and a virtualized body — the component owns the UX, your model owns the data.

```slint
import { DataTable, DataTableRow } from "@glint/components/data-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: 700px;
    height: 400px;
    background: Tokens.color-background;

    in-out property <[DataTableRow]> rows: [
        { label: "ken99@yahoo.com", cells: [
            { kind: TableCellKind.badge, text: "Success" },
            { text: "ken99@yahoo.com" },
            { text: "$316.00" },
        ] },
        { label: "abe45@gmail.com", cells: [
            { kind: TableCellKind.badge, text: "Success" },
            { text: "abe45@gmail.com" },
            { text: "$242.00" },
        ] },
        { label: "monserrat44@gmail.com", cells: [
            { kind: TableCellKind.badge, text: "Processing",
              badge-variant: BadgeVariant.secondary },
            { text: "monserrat44@gmail.com" },
            { text: "$837.00" },
        ] },
        { label: "carmella@hotmail.com", cells: [
            { kind: TableCellKind.badge, text: "Failed",
              badge-variant: BadgeVariant.destructive },
            { text: "carmella@hotmail.com" },
            { text: "$721.00" },
        ] },
    ];

    VerticalLayout {
        padding: 24px;
        alignment: start;

        DataTable {
            accessible-label: "Payments";
            columns: [
                { title: "Status", width: 130px },
                { title: "Email" },
                { title: "Amount", align: TableAlign.end, width: 120px },
            ];
            rows <=> root.rows;
            selectable: true;
            selected-count: (root.rows[0].selected ? 1 : 0)
                + (root.rows[1].selected ? 1 : 0)
                + (root.rows[2].selected ? 1 : 0)
                + (root.rows[3].selected ? 1 : 0);
            all-rows-selected(on) => {
                root.rows[0].selected = on;
                root.rows[1].selected = on;
                root.rows[2].selected = on;
                root.rows[3].selected = on;
            }
        }
    }
}
```

## Usage

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

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

    // The page your model has already sorted, filtered and sliced.
    in-out property <[DataTableRow]> page-of-rows;
    in property <int> total-pages: 1;
    in property <int> selected-count: 0;
    in property <int> row-count: 0;

    callback re-sort(int);
    callback slice(int);

    VerticalLayout {
        padding: 24px;

        DataTable {
            accessible-label: "Payments";
            columns: [
                { title: "Email" },
                { title: "Amount", align: TableAlign.end, width: 120px },
            ];
            rows <=> root.page-of-rows;
            total-pages: root.total-pages;
            selectable: true;
            selected-count: root.selected-count;
            row-count: root.row-count;
            sort(column) => { root.re-sort(column); }
            page-changed(page) => { root.slice(page); }
        }
    }
}
```

**The component handles the UX; you own the data.** Clicking a header, ticking a row, hiding a column and stepping a page are all the table’s, and each of them reports what the user asked for — but Slint has no substring methods and no sort primitive on arrays, so filtering, sorting and slicing live in your model (ADR-0016). `rows` is the page you have already prepared, not the whole set.

Two of those callbacks need a word each:

- **`sort(column)`** fires *after* the table has settled `sort-column` and `sort-desc` for you, including the flip that a second click on the same header means. Re-sort your model from those two.
- **`all-rows-selected(on)`** is the one the table cannot settle for you. The rows it would have to write are the rows a virtualized body has never built, and ticking only what is on screen is worse than ticking nothing (ADR-0028) — so your model is what turns the intent into rows. `selected-count` and `row-count` are sums only you can take, for the same reason; leave `row-count` at `0` and the selection summary counts against the page it was handed.

Cells are `TableCell` values shared with [Table](/docs/components/table), so a status badge, an icon, a checkbox or a per-row action menu stands in one. The selection column is the table’s own, not a cell of your model — which is why a row’s tick reports through `row-selected` and a checkbox *cell* reports through `cell-toggled`.

## Examples

### Sorting

Clicking a header fires `sort(column)`; a second click on the same header reverses the direction. The table moves the chevron, and the model here answers by handing back a differently ordered array.

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

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

    in-out property <int> sort-column: 0;
    in-out property <bool> sort-desc: false;

    property <[DataTableRow]> by-name: [
        { cells: [{ text: "Analytics" }, { text: "$1,120" }] },
        { cells: [{ text: "Platform" }, { text: "$3,400" }] },
        { cells: [{ text: "Support" }, { text: "$860" }] },
    ];
    property <[DataTableRow]> by-name-desc: [
        { cells: [{ text: "Support" }, { text: "$860" }] },
        { cells: [{ text: "Platform" }, { text: "$3,400" }] },
        { cells: [{ text: "Analytics" }, { text: "$1,120" }] },
    ];
    property <[DataTableRow]> by-spend: [
        { cells: [{ text: "Support" }, { text: "$860" }] },
        { cells: [{ text: "Analytics" }, { text: "$1,120" }] },
        { cells: [{ text: "Platform" }, { text: "$3,400" }] },
    ];
    property <[DataTableRow]> by-spend-desc: [
        { cells: [{ text: "Platform" }, { text: "$3,400" }] },
        { cells: [{ text: "Analytics" }, { text: "$1,120" }] },
        { cells: [{ text: "Support" }, { text: "$860" }] },
    ];

    VerticalLayout {
        padding: 24px;
        alignment: start;

        DataTable {
            accessible-label: "Spend by team";
            columns: [
                { title: "Team" },
                { title: "Spend", align: TableAlign.end, width: 120px },
            ];
            rows: root.sort-column == 1
                ? (root.sort-desc ? root.by-spend-desc : root.by-spend)
                : (root.sort-desc ? root.by-name-desc : root.by-name);
            sort-column <=> root.sort-column;
            sort-desc <=> root.sort-desc;
        }
    }
}
```

### Widget cells

A `TableCell`’s `kind` picks what it draws, and `align` — the column’s, not the cell’s — decides where it sits inside the column’s width. A checkbox cell settles its own state in `rows` before `cell-toggled` fires, and an action cell reports the entry taken through `cell-action`.

```slint
import { DataTable, DataTableRow } from "@glint/components/data-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: 660px;
    height: 340px;
    background: Tokens.color-background;

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

    in-out property <[DataTableRow]> rows: [
        { label: "api-gateway", cells: [
            { kind: TableCellKind.icon, text: "Healthy", icon: IconSet.CircleCheck },
            { text: "api-gateway" },
            { kind: TableCellKind.checkbox, checked: true },
            { text: "1.4 ms" },
            { kind: TableCellKind.actions, actions: [
                { label: "Restart" }, { label: "View logs" },
            ] },
        ] },
        { label: "image-worker", cells: [
            { kind: TableCellKind.icon, text: "Degraded", icon: IconSet.TriangleAlert },
            { text: "image-worker" },
            { kind: TableCellKind.checkbox },
            { text: "38 ms" },
            { kind: TableCellKind.actions, actions: [
                { label: "Restart" }, { label: "View logs" },
            ] },
        ] },
    ];

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

        DataTable {
            accessible-label: "Services";
            columns: [
                { title: "Health", align: TableAlign.center, width: 90px },
                { title: "Service" },
                { title: "Paged", align: TableAlign.center, width: 90px },
                { title: "p99", align: TableAlign.end, width: 90px },
                { title: "", align: TableAlign.end, width: 60px },
            ];
            rows <=> root.rows;
            cell-toggled(row, column, on) => {
                root.last = "Row " + row + " paging " + (on ? "on" : "off") + ".";
            }
            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;
        }
    }
}
```

### Hiding columns

`view-options: true` puts a **View** menu above the table with one checkable row per column. It settles `columns[i].hidden` before reporting through `column-visibility-changed`, so the model is the state — and a hidden column collapses without reshaping a single row.

```slint
import { DataTable, DataTableRow } from "@glint/components/data-table.slint";
import { TableAlign, TableColumn } from "@glint/components/table-cell.slint";
import { Tokens } from "@glint/theme/tokens.slint";

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

    in-out property <[TableColumn]> columns: [
        { title: "Invoice", width: 110px },
        { title: "Customer" },
        { title: "Method", width: 140px },
        { title: "Amount", align: TableAlign.end, width: 110px },
    ];

    VerticalLayout {
        padding: 24px;
        alignment: start;

        DataTable {
            accessible-label: "Invoices";
            view-options: true;
            columns <=> root.columns;
            rows: [
                { cells: [
                    { text: "INV001" }, { text: "Acme Corp" },
                    { text: "Credit Card" }, { text: "$250.00" },
                ] },
                { cells: [
                    { text: "INV002" }, { text: "Globex" },
                    { text: "Bank Transfer" }, { text: "$150.00" },
                ] },
                { cells: [
                    { text: "INV003" }, { text: "Initech" },
                    { text: "PayPal" }, { text: "$350.00" },
                ] },
            ];
        }
    }
}
```

### Paging

`total-pages` and `page-size-options` are what turn the pager on — more than one page to step through, or sizes to pick between. Both `page-changed` and `page-size-changed` fire after the table has settled `current-page` and `page-size`, and re-slicing the model is yours.

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

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

    in-out property <int> page: 0;

    property <[DataTableRow]> first-page: [
        { cells: [{ text: "Ada Lovelace" }, { text: "$316.00" }] },
        { cells: [{ text: "Grace Hopper" }, { text: "$242.00" }] },
    ];
    property <[DataTableRow]> second-page: [
        { cells: [{ text: "Alan Turing" }, { text: "$837.00" }] },
        { cells: [{ text: "Katherine Johnson" }, { text: "$721.00" }] },
    ];
    property <[DataTableRow]> third-page: [
        { cells: [{ text: "Radia Perlman" }, { text: "$188.00" }] },
    ];

    VerticalLayout {
        padding: 24px;
        alignment: start;

        DataTable {
            accessible-label: "Payouts";
            columns: [
                { title: "Recipient" },
                { title: "Amount", align: TableAlign.end, width: 120px },
            ];
            rows: root.page == 0
                ? root.first-page
                : (root.page == 1 ? root.second-page : root.third-page);
            total-pages: 3;
            current-page <=> root.page;
            page-size: 2;
            page-size-options: [2, 5, 10];
        }
    }
}
```

### A model too large to draw

The body is virtualized through `ListView`: it is content-sized while the surrounding layout leaves the table free, and scrolls once the layout constrains its height. Only the rows in view are instantiated, so a ten-thousand-row page costs what a ten-row one does — and the keyboard highlight scrolls itself into view as the arrows walk past an edge.

```slint
import { DataTable, DataTableRow } from "@glint/components/data-table.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;

    VerticalLayout {
        padding: 24px;
        alignment: stretch;

        DataTable {
            accessible-label: "Build queue";
            // The surrounding layout constrains the height, so the body scrolls.
            vertical-stretch: 1;
            columns: [
                { title: "Build" },
                { title: "Branch" },
                { title: "Duration", align: TableAlign.end, width: 110px },
            ];
            rows: [
                { cells: [{ text: "#4102" }, { text: "main" }, { text: "3m 12s" }] },
                { cells: [{ text: "#4101" }, { text: "feat/tables" }, { text: "4m 02s" }] },
                { cells: [{ text: "#4100" }, { text: "main" }, { text: "3m 41s" }] },
                { cells: [{ text: "#4099" }, { text: "fix/scroll" }, { text: "2m 58s" }] },
                { cells: [{ text: "#4098" }, { text: "main" }, { text: "3m 09s" }] },
                { cells: [{ text: "#4097" }, { text: "feat/tree" }, { text: "5m 20s" }] },
                { cells: [{ text: "#4096" }, { text: "main" }, { text: "3m 33s" }] },
                { cells: [{ text: "#4095" }, { text: "chore/deps" }, { text: "1m 47s" }] },
            ];
        }
    }
}
```

## API Reference

### Properties

| Property              | Type                    | Default                  | Description                                                                                                                                                                                                                                                                                   |
| --------------------- | ----------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `columns`             | `in-out [TableColumn]`  | no default               | Columns: their headings, widths, alignment and visibility. Clicking a header fires `sort`. `in-out` because the view-options menu settles `hidden` here before reporting it — the model is the state, the way a menu's checkbox rows are (ADR-0020).                                          |
| `rows`                | `in-out [DataTableRow]` | no default               | Visible rows (already sorted / filtered / paged by the consumer). `in-out` because a row's own checkbox and a checkbox cell settle here before reporting.                                                                                                                                     |
| `sort-column`         | `in-out int`            | `-1`                     | Current sort column index (-1 = unsorted). Two-way so the indicator stays accurate as the consumer flips state.                                                                                                                                                                               |
| `sort-desc`           | `in-out bool`           | `false`                  | Current sort direction; flipping is automatic when the same header is clicked twice in a row.                                                                                                                                                                                                 |
| `selectable`          | `in bool`               | `false`                  | Renders the leading selection column: a select-all box in the header and one box per row.                                                                                                                                                                                                     |
| `selected-count`      | `in int`                | `0`                      | How many rows of the whole model are ticked, and how many there are. Both are sums the consumer takes, for the same reason sorting and paging are theirs (ADR-0016): a virtualized body cannot count rows it has not instantiated. `row-count` left at 0 falls back to the page it was given. |
| `row-count`           | `in int`                | `0`                      |                                                                                                                                                                                                                                                                                               |
| `view-options`        | `in bool`               | `false`                  | Renders the view-options menu — one checkable row per column, hiding and showing it without reshaping a single row of data.                                                                                                                                                                   |
| `total-pages`         | `in int`                | `1`                      | How many pages the model holds; the pager's steps and its page label count against it. More than one is one of the two things that turn the pager on — see `page-size-options` for the other.                                                                                                 |
| `current-page`        | `in-out int`            | `0`                      | Two-way; the active page (0-indexed).                                                                                                                                                                                                                                                         |
| `page-size-options`   | `in [int]`              | `[]`                     | Rows-per-page choices the pager offers, and the one in force. Left empty the pager shows no size control — and offering sizes is itself what turns the pager on, because a model that currently fits one page is exactly when a reader wants a smaller one.                                   |
| `page-size`           | `in-out int`            | `10`                     |                                                                                                                                                                                                                                                                                               |
| `select-all-label`    | `in string`             | `@tr("Select all rows")` | Names for the controls the table ships. Override to translate.                                                                                                                                                                                                                                |
| `view-options-label`  | `in string`             | `@tr("View")`            |                                                                                                                                                                                                                                                                                               |
| `rows-per-page-label` | `in string`             | `@tr("Rows per page")`   |                                                                                                                                                                                                                                                                                               |
| `first-page-label`    | `in string`             | `@tr("First page")`      |                                                                                                                                                                                                                                                                                               |
| `previous-page-label` | `in string`             | `@tr("Previous page")`   |                                                                                                                                                                                                                                                                                               |
| `next-page-label`     | `in string`             | `@tr("Next page")`       |                                                                                                                                                                                                                                                                                               |
| `last-page-label`     | `in string`             | `@tr("Last page")`       |                                                                                                                                                                                                                                                                                               |
| `actions-label`       | `in string`             | `@tr("Row actions")`     | Name an action cell's button falls back to when it carries no text.                                                                                                                                                                                                                           |

### Callbacks

| Callback                               | Description                                                                                                                                                                                                                                                                                 |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sort(int)`                            | Fired with the column index when a header is clicked. The component updates `sort-column` / `sort-desc` first.                                                                                                                                                                              |
| `row-selected(int, bool)`              | Fired when a row's own box is ticked or cleared; the row settles first.                                                                                                                                                                                                                     |
| `all-rows-selected(bool)`              | Fired when the select-all box is used. This one the table cannot settle for you: the rows it would have to write are the rows a virtualized body has not built, and ticking only what is on screen is worse than ticking nothing (ADR-0028). Your model is what turns the intent into rows. |
| `column-visibility-changed(int, bool)` | Fired with the column and whether it is visible now; `columns` has already settled.                                                                                                                                                                                                         |
| `page-changed(int)`                    | Fired when the user picks a different page.                                                                                                                                                                                                                                                 |
| `page-size-changed(int)`               | Fired with the new page size; `page-size` has already settled.                                                                                                                                                                                                                              |
| `row-clicked(int)`                     | Fired with the row index when a row is clicked, activated from the keyboard or taken through its accessible default action.                                                                                                                                                                 |
| `cell-toggled(int, int, bool)`         | A checkbox \*cell\* was flipped: its row, its column and the state it now holds. Distinct from `row-selected`, which is the selection column the table owns rather than a cell of the model.                                                                                                |
| `cell-action(int, int, int, int)`      | An action cell fired: its row, its column, the entry's index and the submenu leaf's index, or -1 when the entry itself was taken.                                                                                                                                                           |

`DataTableRow` is a data type rather than a component: `cells`, `label` (what a screen reader calls the row; it falls back to the first cell) and `selected` (read, never written from here for a select-all). `TableColumn`, `TableCell`, `TableAlign` and `TableCellKind` are shared with [Table](/docs/components/table#usage), where their fields are spelled out.

### ListView

The virtualized viewport the body scrolls in, and the one Glint publishes for row models of your own. It is [ScrollArea](/docs/components/scroll-area) under the one name the Slint compiler recognizes and windows: the compiler writes `viewport-height` and each row’s `y` itself, so only the rows in view are instantiated (ADR-0019). The price of the name is that a `ListView` takes exactly one `for` and nothing else — no sibling, no `if`, no plain child, and no `@children` slot per row. Content that is not a row model goes in a `ScrollArea` instead: same surface, same scrollbar, without the windowing.

```slint
import { ListView } from "@glint/components/list-view.slint";
import { Tokens } from "@glint/theme/tokens.slint";

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

    in property <[string]> items;

    ListView {
        for item in root.items: Text {
            height: 32px;
            text: item;
            color: Tokens.color-foreground;
            vertical-alignment: center;
        }
    }
}
```

### Properties

| Property               | Type            | Default    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------- | --------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `viewport-height`      | `in-out length` | no default | Total height of the scrollable content, measured off the children's layout unless a call site states it. It scrolls once this exceeds `visible-height`. A `ListView` has the compiler write it instead, from the rows it has instantiated.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `viewport-width`       | `in-out length` | no default | Total width of that content, measured and scrolled the same way.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `viewport-y`           | `in-out length` | no default | How far the content is scrolled, as a non-positive offset — 0 at the top, `visible-height - viewport-height` at the bottom. Two-way, so a host that owns a keyboard cursor can scroll it into view; the wheel and the scrollbar write it too.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `viewport-x`           | `in-out length` | no default | The same, sideways: 0 at the leading edge, `visible-width - viewport-width` at the trailing one.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `visible-height`       | `out length`    | no default | The window onto the content — what `viewport-y` and `viewport-x` slide.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `visible-width`        | `out length`    | no default |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `scrollbar-hide-delay` | `in duration`   | `0ms`      | How long a bar stays after the reader stops scrolling. `0ms` — the default — is a bar that stays for as long as the content overflows; anything else is a bar that appears when the surface is scrolled and fades out once the reader has left it alone that long.                                                                                                                                                                                                                                                                                                                                                                                               |
| `keyboard-step`        | `in length`     | `40px`     | How far one arrow key, or one accessible increment, moves the content. A page is a window less one of these, so what was at the edge stays on screen and the reader keeps their place.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `content-takes-focus`  | `in bool`       | `false`    | Whether the content has tab stops of its own — a `Textarea`'s field, a list of rows, a menu. ADR-0032 gives this surface a stop in the reading order, and the scope that takes it wraps the content so keys the content refuses bubble out to it. But an ancestor is reached \*first\* by Slint's pre-order tab walk, so where the content is focusable that stop lands in front of it: Tab reached an invisible scroller instead of the field, no ring drew, and typed characters went nowhere until a second Tab. Set this where the content answers the keyboard; the surface keeps the keys it is handed and stops claiming a stop the content already owns. |
| `scrolls-down`         | `out bool`      | no default | Which axes have somewhere to go. A bar takes room from the other bar's track, so each also has to know about the other.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `scrolls-sideways`     | `out bool`      | no default |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

### Callbacks

| Callback     | Description                                                                                                                                                                                             |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scrolled()` | Fires when the reader scrolls — by wheel, by thumb or from the keyboard — and never for an offset written from code, which is what lets a host tell the reader's intent apart from its own corrections. |

### Functions

| Function                                 | Description                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reveal(offset: length, extent: length)` | Scroll just enough to bring the band from `offset` to `offset + extent` — a row, measured in the content's own coordinates — inside the window, and not a pixel further. Every Glint list that carries a keyboard highlight has the same job when the highlight walks past an edge, so the arithmetic lives here rather than three times over. |
| `bounded-y(to: length) -> length`        | The offsets the content's own ends allow, along each axis. Offsets run non-positive, so the far end is the floor and 0 is the start; content that fits has both at 0 and every walk below is a no-op.                                                                                                                                          |
| `bounded-x(to: length) -> length`        |                                                                                                                                                                                                                                                                                                                                                |

Every row above is inherited: a `ListView` declares nothing of its own, and what it adds to [ScrollArea](/docs/components/scroll-area) is the name, `accessible-role: list`, and the compiler’s windowing behind it. The table lists them here because this is where a reader meets a `ListView` — reaching through `inherits` is what keeps it from being a heading over nothing.

### Cell primitives

What `Table` and `DataTable` both draw their columns and cells with, so the two cannot drift apart in what a cell may hold, how a column is sized or what a screen reader hears (ADR-0028). Reach for them when you are building a table surface of your own; `TableGeometry` is the global that holds the one inset every table’s content stands from its column’s edges.

`TableCellView` draws one cell: a string, or the widget a `TableCell` names.

### Properties

| Property        | Type            | Default              | Description                                                                                                                       |
| --------------- | --------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `cell`          | `in TableCell`  | no default           |                                                                                                                                   |
| `align`         | `in TableAlign` | no default           | Alignment inherited from the column the cell sits in.                                                                             |
| `column`        | `in string`     | no default           | Title of that column — what a screen reader hears before the value, and the name a control with no text of its own falls back to. |
| `column-index`  | `in int`        | no default           | Position of that column, published so a screen reader can say which one this is.                                                  |
| `actions-label` | `in string`     | `@tr("Row actions")` | Name for an action cell's button when the cell carries no text.                                                                   |

### Callbacks

| Callback                    | Description                                                                                                                   |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `toggled(bool)`             | A checkbox cell was flipped, with the state the user asked for.                                                               |
| `action-selected(int, int)` | An action was taken: the entry's index, and the index of the submenu leaf it was taken from, or -1 when the entry itself was. |

### Enums

| Enum         | Values                   |
| ------------ | ------------------------ |
| `TableAlign` | `start`, `center`, `end` |

`TableCellRow` lays a row of them out against the columns’ geometry, and draws the header run as well as the body’s.

### Properties

| Property        | Type               | Default    | Description |
| --------------- | ------------------ | ---------- | ----------- |
| `columns`       | `in [TableColumn]` | no default |             |
| `cells`         | `in [TableCell]`   | no default |             |
| `actions-label` | `in string`        | no default |             |

### Callbacks

| Callback                         | Description                                                                   |
| -------------------------------- | ----------------------------------------------------------------------------- |
| `toggled(int, bool)`             | A checkbox cell in column `int` was flipped.                                  |
| `action-selected(int, int, int)` | An action cell in column `int` fired: the entry, then the submenu leaf or -1. |

`TableColumnCell` is the box one column reserves in that row.

### Properties

| Property | Type             | Default    | Description |
| -------- | ---------------- | ---------- | ----------- |
| `column` | `in TableColumn` | no default |             |

`TableHeadLabel` is the header’s own type treatment, so a column title cannot drift from the rest of the table.

### Properties

| Property | Type            | Default    | Description                                                          |
| -------- | --------------- | ---------- | -------------------------------------------------------------------- |
| `align`  | `in TableAlign` | no default | Which way the column reads, so the heading sits over its own values. |

### Enums

| Enum         | Values                   |
| ------------ | ------------------------ |
| `TableAlign` | `start`, `center`, `end` |

`TableGeometry` is the global holding the one inset every table’s content stands from its column’s edges. It is read, never set: geometry belongs to the column (ADR-0028), and two tables reading two insets is how their text stopped lining up.

### Properties

| Property | Type         | Default    | Description |
| -------- | ------------ | ---------- | ----------- |
| `inset`  | `out length` | no default |             |

## Accessibility

- **Table role.** The root carries `accessible-role: table` with `accessible-item-count` as the number of rows; the body’s `ListView` is silenced in its favour so there is not a second list to read through. Naming the table is the call site’s — set `accessible-label`.
- **Headers are buttons.** A sortable header carries `accessible-role: button` named by its column title, and sorting is its default action, so assistive technology can sort without the pointer. A hidden column draws no node at all.
- **Rows are items.** Each row is a `list-item` carrying its index, its name (`label`, falling back to the first cell) and — when the table is `selectable` — whether it is in the selection. Acting on a row is its default action.
- **Cells.** A cell that draws no control is announced as its column then its value (“Status: Success”) with the column’s position; a checkbox cell announces itself a checkbox and an action cell a button, each carrying the column’s position the same way. Slint 1.17 has no cell or column-header role, so that pairing is as close to header association as the platform gets.
- **Keyboard — the header strip.** `Tab` reaches it first. `Left` and `Right` walk the columns, stepping over hidden ones, and `Enter` or `Space` sorts by the one in focus — the same flip-on-repeat rule a click follows. A keyboard-only focus ring is drawn around the column the arrows landed on, not around the whole strip.
- **Keyboard — the body.** `Tab` again reaches the rows as one stop. `Up` and `Down` move the highlight and `Home` / `End` reach the ends, all clamped — a table walks its rows, it does not cycle them. `Enter` acts on the row the highlight stands on, and `Space` ticks it when the table is selectable. The highlight is not the selection: standing on a row implies neither.
- **Keyboard — the menus.** The **View** menu and the rows-per-page list answer the same ladder: `Up` / `Down` wrap through the rows, `Home` and `End` are its ends, and `Enter` or `Space` takes the row under the highlight. Each opens on its first row rather than on last time’s.
- **The controls are named and translatable.** `select-all-label`, `view-options-label`, `rows-per-page-label`, the four pager labels and `actions-label` all ship through `@tr(...)` and stay overridable.
