Datatable
UiDatatable renders rows from an items array against an explicit columns definition. Each column's key is the data field, the sort key, and the cell-<key> slot name; label is the header text.
Add a datatable
<script setup lang="ts">
import type { IDatatableColumn } from '@colorffy/ui'
const columns: IDatatableColumn[] = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'total', label: 'Total', align: 'end' },
]
const items = [
{ name: 'John Doe', email: 'john@example.com', total: 1280 },
{ name: 'Jane Smith', email: 'jane@example.com', total: 940 },
]
</script>
<template>
<UiDatatable
:columns="columns"
:items="items"
default-sort-key="name"
table-class="table-bordered"
/>
</template>
Customize a cell
The cell slot name is cell-<key>:
<template>
<UiDatatable :columns="columns" :items="items">
<template #cell-total="{ item }">
<strong>${{ item.total }}</strong>
</template>
</UiDatatable>
</template>
Hide, align, and manage columns
Mark a column hidden to start it collapsed, then enable column-manager so users can toggle it:
<script setup lang="ts">
import type { IDatatableColumn } from '@colorffy/ui'
const columns: IDatatableColumn[] = [
{ key: 'id', label: 'ID', hidden: true },
{ key: 'name', label: 'Name' },
{ key: 'actions', label: 'Actions', hideLabel: true, hideable: false, fit: true, sortable: false, align: 'end' },
]
</script>
<template>
<UiDatatable :columns="columns" :items="items" column-manager>
<template #cell-actions="{ item }">
<UiButtonTooltip variant="text" size="sm" icon :tooltip-text="`Edit ${item.name}`">
<template #icon>
<UiIconMaterial icon-code="" />
</template>
</UiButtonTooltip>
</template>
</UiDatatable>
</template>
Utility columns such as row actions or toggles take three options:
hideLabelkeeps the label for screen readers but hides it in the header, so the column still has a name.hideable: falsekeeps the column on screen: it is left out of the column manager and the show-all toggle. A column with an emptylabelis never hideable, since the menu could not name it.fitmakes the column only as wide as its content, without wrapping.
The column manager always keeps one hideable column visible.
Customize the toolbar
The toolbar above the table has two sides. #controls fills the left side (search, filters, bulk actions). On the right, #actions-start and #actions-end add your own buttons to the same button group as the built-in column buttons, before and after them. The toolbar renders as soon as any of these slots is used, even without column-manager.
<template>
<UiDatatable :columns="columns" :items="items" column-manager>
<template #controls>
<UiInputText v-model="search" placeholder="Search" hide-label />
</template>
<template #actions-start>
<UiButtonTooltip
id="export"
variant="outline"
size="sm"
icon
tooltip-text="Export CSV"
>
<template #icon>
<UiIconMaterial icon-code="" />
</template>
</UiButtonTooltip>
</template>
</UiDatatable>
</template>
To restyle the built-in buttons, pass toolbarButton. Its variant, color, size, customClass and rounded override the default outline, small style on both the column toggle and the column manager:
<template>
<UiDatatable
:columns="columns"
:items="items"
column-manager
:toolbar-button="{ variant: 'tonal', size: 'md' }"
/>
</template>
For a lighter toolbar, use text buttons in the slots and in toolbarButton. A group of only text buttons sits closer, like a row of icon actions:
<template>
<UiDatatable
:columns="columns"
:items="items"
column-manager
:toolbar-button="{ variant: 'text', customClass: 'text-neutral' }"
>
<template #actions-start>
<UiButtonTooltip variant="text" custom-class="text-neutral" size="sm" icon tooltip-text="Refresh">
<template #icon>
<UiIconMaterial icon-code="" />
</template>
</UiButtonTooltip>
</template>
</UiDatatable>
</template>
To replace a built-in button entirely, use the #column-toggle or #column-manager scoped slot. Both receive the column state (IDatatableColumnSlotProps), so your markup drives the same visibility logic:
| Slot prop | Type | Description |
|---|---|---|
columns | IDatatableColumn[] | The columns the user can show or hide, including hidden ones. |
allVisible | boolean | True when no column is hidden. |
isVisible | (key: string) => boolean | Whether a column is shown. |
isLocked | (key: string) => boolean | True for the last visible column, which cannot be hidden. |
toggle | (key: string) => void | Show or hide one column. |
toggleAll | () => void | Show every column, or restore the default hidden ones. |
<template>
<UiDatatable :columns="columns" :items="items" column-manager>
<template #column-manager="{ columns, isVisible, isLocked, toggle }">
<UiChip
v-for="column in columns"
:key="column.key"
:text="column.label"
:selected="isVisible(column.key)"
:disabled="isLocked(column.key)"
@click="toggle(column.key)"
/>
</template>
</UiDatatable>
</template>
Select rows
Set selectable and bind v-model:selected to an array of row identities. A row's identity is the same value used for its :key — rowKey if set, else the row's id, else its array index (see Props). The header checkbox selects or clears every row on screen (the current page, with pagination) and shows an indeterminate state when only some are selected; selection stays attached to the row across sorting as long as rows have a stable id (or rowKey).
<script setup lang="ts">
import { ref } from 'vue'
const selected = ref<(string | number)[]>([])
</script>
<template>
<UiDatatable
:columns="columns"
:items="items"
selectable
v-model:selected="selected"
/>
</template>
Split rows into pages
Set pagination with a pageSize to show the rows a page at a time, with a pagination under the table. Sorting covers every row, not just the current page. Bind v-model:page to read the page or set it, for example from the URL.
<script setup lang="ts">
const page = ref(1)
</script>
<template>
<UiDatatable
v-model:page="page"
:columns="columns"
:items="items"
:pagination="{ pageSize: 20, showEdges: true }"
/>
</template>
The page goes back to 1 when the sort, the page size or the number of rows changes, so a filter always starts from its first match; editing a row in place keeps the page. The pagination hides itself when everything fits on one page. Besides pageSize, the object takes the pagination's siblingCount, showEdges, compact, size, ariaLabel and labels.
To page on the server, leave pagination off, pass the current page's rows as items, and render a UiPagination yourself.
Stick the header while scrolling
Set stickyHeader to keep the header visible while the body scrolls. It automatically wraps the table in a bounded, vertically-scrollable container (.table-responsive-sticky); set its max height with stickyHeight (default 32rem). It takes any CSS length; a number is read as pixels:
<template>
<UiDatatable
:columns="columns"
:items="items"
sticky-header
sticky-height="20rem"
/>
</template>
Show loading and empty states
<template>
<!-- Skeleton rows while data loads -->
<UiDatatable :columns="columns" :items="[]" :is-loading="true" :skeleton-rows="6" />
<!-- Built-in empty state -->
<UiDatatable
:columns="columns"
:items="[]"
empty-state-title="No customers yet"
empty-state-subtitle="New customers will appear here."
/>
</template>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | IDatatableColumn[] | — | Column definitions (required). |
items | Record<string, any>[] | — | Row data (required). |
rowKey | string | id → index | Stable key field for rows; also the row-selection identity. |
selectable | boolean | false | Show a leading checkbox column. Pair with v-model:selected. |
selected | (string | number)[] | [] | Selected row identities. Bind with v-model:selected. |
pagination | IDatatablePagination | null | null | Show the rows a page at a time: pageSize plus pagination options. |
page | number | 1 | Current page with pagination. Bind with v-model:page. |
stickyHeader | boolean | false | Stick the header while the body scrolls (wraps in .table-responsive-sticky). |
stickyHeight | string | number | null | null (32rem) | Max height of the sticky scroll container. Numbers are pixels. |
sortable | boolean | true | Enable sorting (per-column opt-out via column.sortable). |
defaultSortKey | string | '' | Column key to sort by initially. |
defaultSortOrder | 'asc' | 'desc' | 'asc' | Initial sort direction. |
columnManager | boolean | false | Show the icon-only show / hide column menu. |
columnManagerTooltip | string | 'Manage columns' | Tooltip and accessible name of the column manager button. |
columnsToggleTooltip | string | { showAll, hideDefault } | { showAll: 'Show all columns', hideDefault: 'Hide default columns' } | Tooltip of the show-all / restore button, which appears when a column is hidden. A string is used for both states. |
toolbarButton | DatatableToolbarButton | null | null | variant, color, size, customClass, rounded for the built-in toolbar buttons. |
tableClass | 'table-bordered' | 'table-striped' | 'table-borderless' | string | '' | Table style. |
isLoading | boolean | false | Show skeleton rows, one cell per visible column. |
skeletonRows | number | 10 | Number of skeleton rows. |
caption | string | — | Caption for the table: screen readers read it as the <caption>, and its text shows below the table, outside the horizontal scroll (.table-caption). |
emptyStateTitle | string | 'No data available' | Empty-state title. |
emptyStateSubtitle | string | … | Empty-state subtitle. |
emptyStateUseCustomIcon | boolean | false | Show emptyStateIconCode instead of the default empty-state illustration. |
emptyStateIconCode | string | '' | Material icon code for the empty state, with emptyStateUseCustomIcon. |
Column (IDatatableColumn): key, label, hideLabel?, sortable?, hidden?, hideable?, fit?, align? (start / center / end), thClass?, tdClass?.
Slots: cell-<key> per column; controls, actions-start, actions-end; column-toggle and column-manager (scoped, IDatatableColumnSlotProps, whose columns lists the hideable columns).
CSS variables
These style the .table the datatable renders, and any plain .table. Set them on :root, or on any wrapper to restyle only the tables inside it. A wide table scrolls sideways inside .table-responsive, which hides its scrollbar (touch, trackpads and Shift + wheel still scroll it), and a select in a cell keeps at least the width of its longest option. The sm and lg sizes keep their own cell padding, and the bordered and borderless styles their own corners (bordered also its header background). The stickyHeight prop wins over --cffy-table-sticky-max-height. Hover and pressed backgrounds come from the shared state layer.
:root {
--cffy-table-header-bg-color: var(--cffy-surface-container);
--cffy-table-radius: var(--cffy-radius-md);
--cffy-table-cell-padding-block: var(--cffy-space-12);
}
| Variable | Default | Controls |
|---|---|---|
--cffy-table-bg-color / --cffy-table-color | transparent / currentColor | Table background and text |
--cffy-table-border-color / --cffy-table-border-width | --cffy-outline-surface / --cffy-border-width-sm | Row and cell borders |
--cffy-table-cell-padding-inline / --cffy-table-cell-padding-block | --cffy-space-8 / --cffy-space-8 | Cell padding (header cells use 1.5× the block padding) |
--cffy-table-font-size | 0.75rem | Body text |
--cffy-table-radius | --cffy-radius-none | Header corners |
--cffy-table-header-bg-color / --cffy-table-header-color | --cffy-surface-a10 / --cffy-muted-a10 | Header background and text |
--cffy-table-header-font-size / --cffy-table-header-font-weight | --cffy-fs-4xs / --cffy-fw-600 | Header text |
--cffy-table-icon-color / --cffy-table-icon-size | --cffy-muted-a10 / 1.25rem | Icons in .td-name cells |
--cffy-table-selected-bg-color | --cffy-primary-container | Selected rows |
--cffy-table-striped-bg-color | --cffy-surface-container | Odd rows of .table-striped |
--cffy-table-easing / --cffy-table-duration | ease-in-out / --cffy-duration-100 | Row hover transition |
--cffy-table-sticky-max-height | 32rem | Height of the scroll area with stickyHeader |
--cffy-table-sticky-column-bg-color | --cffy-surface-body-pane | Pinned .highlight column while scrolled in .table-scrollable |