Data Display
DataTable
A data grid with sorting, selection, resizing, and virtualization, composed from Columns.
Install
import { DataTable } from 'vael-ui'Playground
checkbox
md
always
clone
import { DataTable } from 'vael-ui'
<DataTable :loading="false" :selectable="false" selectionMode="checkbox" :single="false" scrollHeight="" stackedBreakpoint="" size="md" :stripedRows="false" :showGridlines="false" :resizableColumns="false" :frozenColumns="0" :rows="0" :manualSort="false" :lazy="false" :total="0" :virtualize="false" motionCss :reorderableColumns="false" columnGripVisibility="always" previewMode="clone" :touchDragDelay="150" :page="1">DataTable</DataTable>Props
| Name | Type | Default | Description |
|---|---|---|---|
data | T[] | Row objects. Column content is read from these via each `<Column>`'s `field`. | |
rowKey | keyof T | ((row: T) => string | number) | Stable row identity — a key on `T`, or a function for composite/derived keys. | |
loading | boolean | undefined | false | Shows the `#loading` slot instead of rows/empty state. |
selectable | boolean | undefined | false | Adds a leading checkbox/radio column wired to the `selected` state. |
selectionMode | "checkbox" | "row" | "checkbox" | `'checkbox'` (default): leading selection column. `'row'`: click row to toggle selection. |
single | boolean | undefined | false | Single-selection mode — `selected` holds at most one key. In `'checkbox'` mode uses `Radio`. |
scrollHeight | string | undefined | CSS length (`'400px'`, `'60vh'`). When set, body scrolls with sticky header; unset uses natural flow. | |
stackedBreakpoint | string | undefined | CSS length (`'640px'`). Below this viewport width, switches to stacked card layout. | |
size | "md" | "sm" | "lg" | "md" | Row-density variant. |
stripedRows | boolean | undefined | false | Alternating row background via CSS (selected > hover > stripe precedence). |
showGridlines | boolean | undefined | false | Adds inline-end border to every cell. |
resizableColumns | boolean | undefined | false | Adds a drag handle to every column header's edge for resizing. |
frozenColumns | number | undefined | 0 | Freezes the first N columns sticky-left against horizontal scroll. |
rows | number | undefined | Rows per page. Default: all rows render. Set: internal slicing; pair with `v-model:page`. Also the page-size divisor for `lazy`'s `total`. | |
manualSort | boolean | undefined | false | `data` is already sorted server-side — DataTable stops sorting it locally and only reflects `v-model:sort`, so a header click tells you what to refetch instead of re-sorting what you gave it. |
lazy | boolean | undefined | false | `data` is already just the current page — DataTable stops slicing it locally. Pair with `total` (the real across-all-pages count) so `#footer`/`Pagination` math stays correct. |
total | number | undefined | Real row count across all pages. Only meaningful with `lazy`; falls back to `sortedData.length` (i.e. `data.length`) when unset. | |
virtualize | boolean | { itemSize?: number | undefined; overscan?: number | undefined; estimateSize?: number | undefined; } | undefined | Windows rendering to the visible rows + overscan, for very large `data`. Requires `scrollHeight`. `true` measures each row's real height (rows may vary, e.g. wrapping `#cell` content or `stackedBreakpoint`); pass an object to tune it. | |
motionCss | boolean | undefined | true | Gates the built-in row enter/exit/reorder transition (sort, paging, row expansion). `false` skips it entirely — reach for `@row-enter`/`@row-leave` instead if you want a consumer-owned animation (GSAP, motion-v) in its place. No effect while `virtualize` is active: a virtualized list's rows are measured/recycled by height, which a CSS enter/exit transition would fight, so that mode never animates row presence regardless of this prop. |
reorderableColumns | boolean | undefined | false | Drag column headers to reorder them. Pair with `v-model:columnOrder` to control or persist the order. |
columnGripVisibility | "hover" | "always" | "always" | `'always'` (default): the drag grip is always shown, so a reorderable column reads as such at a glance. `'hover'`: fades in on hover/focus instead, matching the resize handle's own restraint — reach for this once a table has enough reorderable columns that permanent grips would clutter the header. |
canDrop | ((details: SortableDropDetails) => boolean) | undefined | undefined | Structural veto re-run while a column drags; `false` marks the target invalid. A pinned column is already protected regardless of this. |
beforeDrop | ((details: SortableDropDetails) => boolean | Promise<boolean>) | undefined | undefined | Async gate at drop time for a column reorder — return `false` (or a promise of it) to cancel. Composes with `confirmAction().result` for a confirm-before-move dialog. |
previewMode | "element" | "clone" | "clone" | `'clone'` (default): a floating copy of the dragged column header follows the cursor, the real `<th>` hidden until drop. `'element'` moves the real header cell itself instead — **don't use this**: a `<th>`'s `:style` binding is keyed by column index, and lifting the real element out to `position: fixed` mid-drag corrupts that binding badly enough that a column can be lost from the DOM entirely on drop. Kept only for interface symmetry with `Sortable`/`Tree`, where it's safe. |
touchDragDelay | number | undefined | 150 | Ms a touch pointer must hold a column header still before a drag starts. A sortable column's header is also a tap-to-sort button, so touch needs a hold to tell the two apart; mouse/pen are unaffected. Default `150`. |
ui | Partial<{ root: UiPartValue; toolbar: UiPartValue; table: UiPartValue; thead: UiPartValue; th: UiPartValue; sortButton: UiPartValue; grip: UiPartValue; resizeHandle: UiPartValue; tbody: UiPartValue; tr: UiPartValue; td: UiPartValue; expansionRow: UiPartValue; expansionContent: UiPartValue; footer: UiPartValue; }> | undefined | ||
page | number | undefined | 1 | |
sort | { field: keyof T | null; dir: "asc" | "desc" | null; } | undefined | { field: null, dir: null } | Uncontrolled by default (works exactly as before). Bind `v-model:sort` — required with `manualSort` — to see every header click and know what to refetch. |
columnOrder | (keyof T)[] | undefined | [] | Empty means "follow the DOM" (pre-reordering behavior); once a drag sets it, it outranks DOM order so the onUpdated resort below doesn't undo it. |
Slots
| Name | Type | Description |
|---|---|---|
columns | { Column: TypedColumn; columnData: T[]; } | Declare `<Column>` children. `columnData` is the table's `:data`, handed back for type-inference. |
toolbar | { selected: Set<string | number>; count: number; } | Toolbar content (search, bulk actions, …). |
loading | any | Replaces row area while `loading` is true. |
empty | any | Replaces row area when data is empty and not loading. |
footer | { data: T[]; page: number; pageCount: number; total: number; } | Footer content (pagination, …). `data` is sorted (not paginated); `page`/`pageCount` are always provided. |
expansion | { row: T; } | Full-width row beneath an expanded row. In stacked mode, always renders (no toggle). |
Events
| Name | Type | Description |
|---|---|---|
reach-end | [] | |
update:selection | [rows: T[]] | |
row-click | [row: T] | |
reach-start | [] | |
column-reorder | [order: (keyof T)[]] | |
row-enter | [el: Element, done: () => void] | |
row-leave | [el: Element, done: () => void] | |
drop-error | [error: unknown, details: SortableDropDetails] | |
update:page | [value: number] | |
update:sort | [value: { field: keyof T | null; dir: "asc" | "desc" | null; }] | |
update:columnOrder | [value: (keyof T)[]] |
Exposed
| Name | Type | Description |
|---|---|---|
el | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
Column
Defines one column of a DataTable, always used inside its tree.
Props
| Name | Type | Default | Description |
|---|---|---|---|
field | keyof T | ||
label | string | undefined | ||
sortable | boolean | undefined | ||
width | string | number | undefined | ||
resizable | boolean | undefined | undefined | Unset (default) inherits DataTable's `resizableColumns`; `true`/`false` overrides per-column. |
reorderable | boolean | undefined | undefined | Unset (default) inherits DataTable's `reorderableColumns`; `false` pins this column in place. |
data | T[] | undefined | Type-inference anchor only. Bind here (`<Column :data="items" ...>`) so other props infer correctly against `T`. |
Slots
| Name | Type | Description |
|---|---|---|
cell | { row: T; value: T[keyof T]; } | |
header | { column: RegisteredColumn<T>; } |
Exposed
Nothing exposed via template ref.