---
title: DataTable
description: A data table with sorting, selection, and expandable rows.
category: Data
status: beta
since: 0.3.4
tags: [table, data, list]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
::TuffDemoWrapper{demo="DataTableDataTableDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  const columns = [
    { key: 'name', title: 'Name' },
    { key: 'role', title: 'Role' },
    { key: 'score', title: 'Score', sortable: true },
  ]

  const data = [
    { id: 1, name: 'Ava', role: 'Designer', score: 92 },
    { id: 2, name: 'Noah', role: 'Engineer', score: 88 },
    { id: 3, name: 'Mia', role: 'PM', score: 95 },
  ]
  </script>

  <template>
    <TxDataTable :columns="columns" :data="data" striped bordered />
  </template>
---
::

### Row Selection
`selectable` adds a selection column; `v-model:selected-keys` syncs the selected rows.
::TuffDemoWrapper{demo="DataTableDataTableSelectableDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDataTable
      v-model:selected-keys="selectedKeys"
      :columns="columns"
      :data="data"
      row-key="id"
      selectable
    />
  </template>
---
::

### Skeleton Loading
`loadingVariant="skeleton"` draws placeholder rows while there are none; a refresh of existing rows shows only a bar under the header.
::TuffDemoWrapper{demo="DataTableSkeletonLoadingDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDataTable
      :columns="columns"
      :data="rows"
      :loading="loading"
      loading-variant="skeleton"
      :skeleton-rows="pageSize"
      row-key="id"
    />
  </template>
---
::

### Expandable Rows
`expandable` adds a leading toggle column; the `#expanded` slot renders the detail under a row.
::TuffDemoWrapper{demo="DataTableExpandableRowsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDataTable
      v-model:expanded-keys="expandedKeys"
      :columns="columns"
      :data="data"
      row-key="id"
      expandable
      :row-expandable="row => Boolean(row.note)"
    >
      <template #expanded="{ row }">
        {{ row.note }}
      </template>
    </TxDataTable>
  </template>
---
::

### Records Table
A wide layout with a pinned header and footer, a frozen first column, and selection highlighting; cells compose `TxTag`, `TxDotIndicator`, and `TxCellLink`.
::TuffDemoWrapper{demo="DataTableRecordsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDataTable
      v-model:selected-keys="selectedKeys"
      :columns="columns"
      :data="rows"
      :sort="sort"
      row-key="id"
      selectable
      highlight-selected
      table-layout="fixed"
      nowrap
      sort-cycle="bi"
      :max-height="380"
      scroll-x
      sticky-header
      sticky-footer
      @update:sort="sort = $event"
    >
      <template #cell-tags="{ value }">
        <TxTag v-for="tag in value" :key="tag" :label="tag" :dot="TAG_COLORS[tag]" variant="soft" />
      </template>
      <template #cell-strength="{ row }">
        <TxDotIndicator :color="STRENGTH_TONE[row.strength]" :label="STRENGTH_LABEL[row.strength]" />
      </template>
      <template #cell-website="{ value }">
        <TxCellLink :href="`https://${value}`" :label="value" external @open="openSite" />
      </template>

      <template #footer-name>
        <strong>{{ rows.length }}</strong> count
      </template>
      <template #footer-website>
        {{ linkCount }} links
      </template>
    </TxDataTable>
  </template>
---
::

### Data Operations Panel
`TxPagination` pages the list, and `TxSkeleton` / `TxLayoutSkeleton` preview the loading state.
::TuffDemoWrapper{demo="ComponentsDataOperationsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDataTable
      v-model:selected-keys="selectedKeys"
      :columns="columns"
      :data="pagedRows"
      row-key="id"
      selectable
      striped
      bordered
    />
    <TxPagination v-model:current-page="page" :total="rows.length" :page-size="4" show-info />
  </template>
---
::

### Best Practices

- Pass a stable `rowKey` for business lists; to keep selection across pages, drive `selectedKeys` with business ids, not default indexes.
- For server-loaded lists, use `loadingVariant="skeleton"` with `skeletonRows` set to the page size; keep the default `overlay` for short local waits.
- Give fixed columns numeric px `width` / `minWidth`, and keep one horizontal scroller: `scrollX`, or an outer `overflow-x: auto`.
- A pinned header or footer needs `maxHeight` or a scrolling ancestor; don't nest the table in `TxScroll`'s default mode, whose transform scrolling disables sticky.
- Sort on comparable fields such as timestamps or ranks, never on formatted relative-time text.

## API Reference

### Props

| Name | Type | Default | Description |
|------|------|------|------|
| `columns` | `DataTableColumn[]` | `[]` | Column config. |
| `data` | `any[]` | `[]` | Data source. |
| `rowKey` | `keyof T \| (row: T, index: number) => string \| number` | `index` | Unique row key. |
| `loading` | `boolean` | `false` | Loading state. |
| `loadingVariant` | `'overlay' \| 'skeleton'` | `'overlay'` | `overlay` veils the table with a spinner; `skeleton` draws placeholder rows, or only a bar when rows exist. |
| `skeletonRows` | `number` | `5` | Placeholder rows before the first data lands; match the page size so nothing moves. |
| `emptyText` | `string` | `'No data'` | Empty text. |
| `striped` | `boolean` | `false` | Zebra rows. |
| `bordered` | `boolean` | `false` | Show borders. |
| `hover` | `boolean` | `true` | Hover highlight. |
| `interactiveRows` | `boolean` | `false` | Makes rows focusable so Enter / Space fire `rowClick`; on automatically with a `rowClick` listener. |
| `selectable` | `boolean` | `false` | Adds a selection column. |
| `selectedKeys` | `Array<string \| number>` | `[]` | Keys of the selected rows. |
| `expandable` | `boolean` | `false` | Adds a leading toggle column; an expanded row renders a detail row beneath it. |
| `defaultExpandedKeys` | `Array<string \| number>` | `[]` | Initial expanded rows when uncontrolled; `expandedKeys` wins if both are passed. |
| `expandedKeys` | `Array<string \| number>` | - | Controlled expanded rows, bound with `v-model:expanded-keys`. |
| `rowExpandable` | `(row, index) => boolean` | - | Per-row gate for expanding; a rejected row keeps an empty leading cell. |
| `expandLabel` | `string` | `'Expand row'` | Accessible name of a closed toggle. |
| `collapseLabel` | `string` | `'Collapse row'` | Accessible name of an open toggle. |
| `defaultSort` | `{ key: string; order: 'asc' \| 'desc' \| null }` | `null` | Initial sort when uncontrolled; pass either this or `sort`. |
| `sort` | `{ key: string; order: 'asc' \| 'desc' \| null } \| null` | - | Controlled sort; `null` means unsorted. |
| `sortOnClient` | `boolean` | `true` | Sorts locally; set `false` for remote sorting, which only emits. |
| `sortCycle` | `'tri' \| 'bi'` | `'tri'` | `tri`: ascending → descending → unsorted; `bi` alternates and never unsorts. |
| `tableLayout` | `'auto' \| 'fixed'` | `'auto'` | Native `table-layout`; use `fixed` when column widths must stay stable. |
| `nowrap` | `boolean` | `false` | Prevents wrapping in every header and cell. |
| `maxHeight` | `string \| number` | - | Caps the height and scrolls the table vertically; what sticky rows stick to. |
| `scrollX` | `boolean` | `false` | Scrolls horizontally inside the component; for wide tables with fixed columns. |
| `stickyHeader` | `boolean` | `false` | Pins the header row. |
| `stickyFooter` | `boolean` | `false` | Pins the summary row. |
| `rowClass` | `(row, index) => string \| string[] \| Record<string, boolean>` | - | Extra classes per row, such as a tint by state. |
| `highlightSelected` | `boolean` | `false` | Tints selected rows. |

### Events

| Event | Payload | Description |
|------|------|------|
| `update:selectedKeys` | `(keys)` | Selection update. |
| `selectionChange` | `(keys)` | Selection change. |
| `update:expandedKeys` | `(keys)` | Expansion update; fires in controlled and uncontrolled modes. |
| `expand` | `({ row, index, expanded })` | A row was expanded or collapsed. |
| `sortChange` | `(sort)` | Sort change. |
| `update:sort` | `(sort)` | Sort change; fires in controlled and uncontrolled modes. |
| `rowClick` | `({ row, index })` | Row click. |

### Slots

| Name | Description |
|------|------|
| `header-<columnKey>` | Custom header; receives `{ column, sorted, order, toggle }`, where `toggle` advances the sort cycle. |
| `cell-<columnKey>` | Custom cell; receives `{ row, column, value, index }`. |
| `expanded` | Detail of an expanded row, spanning every column; receives `{ row, index }`. |
| `footer` | The whole summary row of your own `<td>`s, which can span columns. Any footer slot renders a `<tfoot>`. |
| `footer-<columnKey>` | One summary cell per column; receives `{ column, data }`. `footer` wins when both exist. |
| `empty` | Empty state when there are no rows and `loading=false`. |

### DataTableColumn

| Field | Type | Description |
|------|------|------|
| `key` | `string` | Column key. |
| `title` | `string` | Header title. |
| `dataIndex` | `string` | Data field. |
| `width` | `string \| number` | Column width. |
| `minWidth` | `string \| number` | Minimum column width. |
| `maxWidth` | `string \| number` | Maximum column width. |
| `auto` | `boolean` | Forces the column width to `auto`. |
| `fixed` | `boolean \| 'left' \| 'right'` | Sticky side; `true` means `'left'`. Left offsets already include the toggle and selection columns. |
| `nowrap` | `boolean` | Prevents wrapping in this column. |
| `align` | `'left' \| 'center' \| 'right'` | Alignment. |
| `sortable` | `boolean` | Sortable. |
| `sorter` | `(a, b) => number` | Custom sorter. |
| `format` | `(value, row, index) => string` | Cell formatter. |
| `headerClass` | `string` | Header class. |
| `cellClass` | `string` | Cell class. |

## Overview

- Passing `sort` or `expandedKeys` (even `null` or `[]`) makes it controlled: the table only emits updates and renders what the parent sends back.
- Sortable headers carry `scope="col"` and `aria-sort`; click, Enter, and Space step through `sortCycle`.
- On a partial selection the select-all box is `indeterminate` (`aria-checked="mixed"`); clicks in the selection and toggle cells never fire `rowClick`.
- `loading` sets `aria-busy` on the `<table>`, and the empty state stays hidden while loading.
- Placeholder rows are `aria-hidden` and as tall as a one-line row; the refresh bar holds still under reduced motion.
- `stickyHeader`, `stickyFooter`, or `maxHeight` switches to `border-collapse: separate`. The last row has no bottom rule; the rule above a `tfoot` stays.

## Technologies

- The skeleton variant renders `TxSkeleton`. The on-demand style plugin loads its sheet; when importing styles by hand, add `@talex-touch/tuffex/skeleton/style.css`.
- Source: `packages/tuffex/packages/components/src/data-table/`.

<TuffDocSourceLink />

## Customization

| CSS variable | Used for |
|------|------|
| `--tx-data-table-row-hover-bg` | Row hover fill. |
| `--tx-data-table-row-selected-bg` | Selected-row fill under `highlightSelected`. |

```css
.records-shell {
  --tx-data-table-row-hover-bg: var(--tx-bui-hover);
  --tx-data-table-row-selected-bg: color-mix(in srgb, var(--tx-bui-accent) 7%, var(--tx-bui-surface));
}
```
