Components

Datatable

Display tabular data with sorting, a column manager, loading, and empty states.

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="&#xe3c9;" />
        </template>
      </UiButtonTooltip>
    </template>
  </UiDatatable>
</template>

Utility columns such as row actions or toggles take three options:

  • hideLabel keeps the label for screen readers but hides it in the header, so the column still has a name.
  • hideable: false keeps the column on screen: it is left out of the column manager and the show-all toggle. A column with an empty label is never hideable, since the menu could not name it.
  • fit makes 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="&#xf090;" />
        </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="&#xe5d5;" />
        </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 propTypeDescription
columnsIDatatableColumn[]The columns the user can show or hide, including hidden ones.
allVisiblebooleanTrue when no column is hidden.
isVisible(key: string) => booleanWhether a column is shown.
isLocked(key: string) => booleanTrue for the last visible column, which cannot be hidden.
toggle(key: string) => voidShow or hide one column.
toggleAll() => voidShow 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

PropTypeDefaultDescription
columnsIDatatableColumn[]—Column definitions (required).
itemsRecord<string, any>[]—Row data (required).
rowKeystringid → indexStable key field for rows; also the row-selection identity.
selectablebooleanfalseShow a leading checkbox column. Pair with v-model:selected.
selected(string | number)[][]Selected row identities. Bind with v-model:selected.
paginationIDatatablePagination | nullnullShow the rows a page at a time: pageSize plus pagination options.
pagenumber1Current page with pagination. Bind with v-model:page.
stickyHeaderbooleanfalseStick the header while the body scrolls (wraps in .table-responsive-sticky).
stickyHeightstring | number | nullnull (32rem)Max height of the sticky scroll container. Numbers are pixels.
sortablebooleantrueEnable sorting (per-column opt-out via column.sortable).
defaultSortKeystring''Column key to sort by initially.
defaultSortOrder'asc' | 'desc''asc'Initial sort direction.
columnManagerbooleanfalseShow the icon-only show / hide column menu.
columnManagerTooltipstring'Manage columns'Tooltip and accessible name of the column manager button.
columnsToggleTooltipstring | { 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.
toolbarButtonDatatableToolbarButton | nullnullvariant, color, size, customClass, rounded for the built-in toolbar buttons.
tableClass'table-bordered' | 'table-striped' | 'table-borderless' | string''Table style.
isLoadingbooleanfalseShow skeleton rows, one cell per visible column.
skeletonRowsnumber10Number of skeleton rows.
captionstring—Caption for the table: screen readers read it as the <caption>, and its text shows below the table, outside the horizontal scroll (.table-caption).
emptyStateTitlestring'No data available'Empty-state title.
emptyStateSubtitlestring…Empty-state subtitle.
emptyStateUseCustomIconbooleanfalseShow emptyStateIconCode instead of the default empty-state illustration.
emptyStateIconCodestring'&#xeb83;'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);
}
VariableDefaultControls
--cffy-table-bg-color / --cffy-table-colortransparent / currentColorTable background and text
--cffy-table-border-color / --cffy-table-border-width--cffy-outline-surface / --cffy-border-width-smRow and cell borders
--cffy-table-cell-padding-inline / --cffy-table-cell-padding-block--cffy-space-8 / --cffy-space-8Cell padding (header cells use 1.5× the block padding)
--cffy-table-font-size0.75remBody text
--cffy-table-radius--cffy-radius-noneHeader corners
--cffy-table-header-bg-color / --cffy-table-header-color--cffy-surface-a10 / --cffy-muted-a10Header background and text
--cffy-table-header-font-size / --cffy-table-header-font-weight--cffy-fs-4xs / --cffy-fw-600Header text
--cffy-table-icon-color / --cffy-table-icon-size--cffy-muted-a10 / 1.25remIcons in .td-name cells
--cffy-table-selected-bg-color--cffy-primary-containerSelected rows
--cffy-table-striped-bg-color--cffy-surface-containerOdd rows of .table-striped
--cffy-table-easing / --cffy-table-durationease-in-out / --cffy-duration-100Row hover transition
--cffy-table-sticky-max-height32remHeight of the scroll area with stickyHeader
--cffy-table-sticky-column-bg-color--cffy-surface-body-panePinned .highlight column while scrolled in .table-scrollable
Copyright © 2026