Table
A sortable data table with a card-styled header and per-column custom cell/header slots.
<UiTable :data="..." :columns="..." />Data
requireddata 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.
Columns
requiredcolumns 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.
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
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.
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.
Sticky
sticky pins the header to the top of its scroll container, so column names stay readable through a long list.
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.
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.
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.
API Reference
Generated from the component's source — props, slots and emits as the component actually declares them.
Props
datarequiredRecord<string, unknown>[](row: Record<string, unknown>) => voidTableSort | nullSlots
${column.accessorKey}-headerloading${column.accessorKey}-cellemptyEmits
update:sort[value: TableSort | null]Types
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;
}interface TableSort {
column: string;
direction: "asc" | "desc";
}