Table

A sortable data table with a card-styled header and per-column custom cell/header slots.

html
<UiTable :data="..." :columns="..." />

Data

required

data is the rows, one plain object each. The table reads values off them through each column's accessorKey and otherwise leaves them alone, so extra keys a column doesn't name are carried along untouched — handy, since the whole row object is what onSelect and the cell slots hand back.

Status
Jane Doe
jane@bundy.dev
Active
$1,204
John Smith
john@bundy.dev
Invited
$820
Ava Chen
ava@bundy.dev
Active
$3,410
Liam Brown
liam@bundy.dev
Suspended
$0

Columns

required

columns defines what's shown and in what order. Each entry needs an accessorKey naming the key to read, plus an optional header (falling back to the key itself), a fixed width such as '120px', an align of start/center/end, and sortable to make its header clickable. Columns without a width share whatever space is left equally.

name
email
status
amount
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Ava Chen
ava@bundy.dev
Active
3410
Liam Brown
liam@bundy.dev
Suspended
0
Status
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Ava Chen
ava@bundy.dev
Active
3410
Liam Brown
liam@bundy.dev
Suspended
0

Column cells default to rendering the raw value as text; override per column with a #<accessorKey>-cell slot for anything richer (badges, formatted numbers, etc.).

Caption

caption is a line of text above the table, naming what the rows are. It's ordinary prose rather than a heading, so it's the place to say what a reader needs before the first row makes sense.

Team members and their account status

Status
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Ava Chen
ava@bundy.dev
Active
3410
Liam Brown
liam@bundy.dev
Suspended
0

Empty

empty is the text shown in place of rows when data is empty. Say what's missing and, where you can, what would fill it — the empty slot takes over entirely if you need a button rather than a sentence.

Status
No members yet

Loading

loading swaps the rows for the loading slot, which is a set of skeleton rows unless you replace it. Prefer it to hiding the table behind a v-if — the header and the column widths stay put, so the page doesn't jump when the data lands.

Status

Sticky

sticky pins the header to the top of its scroll container, so column names stay readable through a long list.

Status
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Ava Chen
ava@bundy.dev
Active
3410
Liam Brown
liam@bundy.dev
Suspended
0

The header renders as its own Card, visually separate from the body rows — that's deliberate, not a layout bug.

On select

onSelect makes rows clickable — passing it is what adds the hover highlight and the pointer cursor, so there's no separate flag to turn that on. It's handed the whole row object, not an index, which is why a row can carry an id the columns never render.

Status
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Ava Chen
ava@bundy.dev
Active
3410
Liam Brown
liam@bundy.dev
Suspended
0
Selected: no row yet

Sort

sort takes the active sort out of the table's hands: pass a { column, direction } object — or null for none — and clicking a sortable header only emits update:sort rather than reordering anything. That's the hook for server-side sorting, where the new order arrives as fresh data.

Status
Ava Chen
ava@bundy.dev
Active
3410
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Liam Brown
liam@bundy.dev
Suspended
0
Sorted by name asc — the reordering below is done outside the table

Sorting is fully client-side and always runs against data — if you're doing server-side sorting, pass sort/@update:sort and just don't rely on the local re-sort changing anything (sorting already-sorted data by the same key is a no-op).

Default sort

defaultSort is the sort applied on mount while the table still manages its own order. Use it to open on the column that makes the list readable — newest first, largest first — instead of whatever order the array happened to arrive in.

Status
Ava Chen
ava@bundy.dev
Active
3410
Jane Doe
jane@bundy.dev
Active
1204
John Smith
john@bundy.dev
Invited
820
Liam Brown
liam@bundy.dev
Suspended
0

API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
datarequired
Record<string, unknown>[]
The rows to render, one object per row, read via each column's `accessorKey`.
columnsrequired
TableColumn[]
The columns to render, in order.
string
Text displayed above the table.
string
'No data'
Text displayed when `data` is empty. Ignored when the `empty` slot is used.
boolean
false
Shows the loading slot instead of rows.
boolean
false
Keeps the header pinned to the top of its scroll container.
(row: Record<string, unknown>) => void
Called when a row is clicked. Rows become clickable (hover highlight, pointer cursor) when set.
TableSort | null
Controls the active sort. Omit this to let the table manage its own sort state internally (starting from `defaultSort`); pass it to fully control it yourself (e.g. for server-side sorting).
TableSort | null
The sort applied when uncontrolled.

Slots

Slot
${column.accessorKey}-header
Dynamic — one slot per item, named from the expression shown.
loading
${column.accessorKey}-cell
Dynamic — one slot per item, named from the expression shown.
empty

Emits

Event
Payload
Description
update:sort
[value: TableSort | null]

Types

ts
interface TableColumn {
    /**
     * The key used to read this column's value off each row in `data`.
     */
    accessorKey: string;

    /**
     * Header text for this column. Falls back to `accessorKey`. Ignored when the `#<accessorKey>-header` slot is used.
     */
    header?: string;

    /**
     * A fixed track size (e.g. `'160px'`) for this column. Defaults to sharing the remaining space equally with other unset columns.
     */
    width?: string;

    /**
     * Text alignment for this column's header and cells.
     * @defaultValue 'start'
     */
    align?: "start" | "center" | "end";

    /**
     * Makes the header clickable to sort by this column.
     * @defaultValue false
     */
    sortable?: boolean;
}
ts
interface TableSort {
    column: string;
    direction: "asc" | "desc";
}