---
title: "Introduction"
description: "A declarative data table based on TanStack Table, with column configuration, selection, expansion, tree data, pagination, load more and complete API reference."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/data-table"
---
# Introduction

> A declarative data table based on TanStack Table, with column configuration, selection, expansion, tree data, pagination, load more and complete API reference.

## Overview

`MDataTable` is a declarative data table built on [`@tanstack/vue-table`](https://tanstack.com/table) and Nuxt UI `UTable`. It consolidates column definitions, sorting, column pinning, column resize, selection, expansion, tree data, pagination and load more into a single `columns` array and a handful of props, while providing complete TypeScript type inference.

> [!NOTE]
> 
> Core philosophy:
> 
> - **Declarative columns** — Describe columns with an array of objects; data columns, selection columns, index columns, expand columns, actions columns and group columns are distinguished by `accessorKey` or `type`.
> - **Global + per-column toggles** — `sortable`, `pinable`, `resizable`, `truncate`, `tooltip` and others can be enabled globally and overridden per column, or driven by a function for dynamic behavior.
> - **Type-safe** — Columns, cell callbacks and event handlers are all driven by types such as `DataTableColumn<T>` and `DataTableProps<T>`.

## Usage

A single `columns` array is enough to declare selection columns, index columns, fixed columns, cell rendering and actions columns. Combine with `row-key` for row identification, `sortable`/`pinable`/`resizable` for sorting, column pinning and column resize, and `v-model:pagination` for pagination — no manual assembly required:

```vue [DataTableBasicExample.vue]
<script setup lang="ts">
import type { DataTableColumn, DataTableDataColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'
import { UBadge } from '#components'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 6 })
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const STATUS_LABEL: Record<Person['status'], string> = {
  active: '在职',
  leave: '休假',
  offboarded: '已离职'
}
const STATUS_COLOR: Record<Person['status'], 'success' | 'warning' | 'neutral'> = {
  active: 'success',
  leave: 'warning',
  offboarded: 'neutral'
}

const statusCell: DataTableDataColumn<Person>['cell'] = ({ getValue }) => {
  const v = getValue<Person['status']>()
  return h(UBadge, { color: STATUS_COLOR[v], variant: 'subtle' }, () => STATUS_LABEL[v])
}
const moneyCell: DataTableDataColumn<Person>['cell'] = ({ getValue }) => `¥${getValue<number>().toLocaleString()}`

const columns: DataTableColumn<Person>[] = [
  { type: 'selection' },
  { type: 'index' },
  { accessorKey: 'name', header: '姓名', size: 120, fixed: 'left', pinable: true },
  { accessorKey: 'department', header: '部门', size: 100 },
  { accessorKey: 'role', header: '岗位', size: 160, resizable: true },
  { accessorKey: 'status', header: '状态', size: 100, cell: statusCell },
  { accessorKey: 'salary', header: '薪资', size: 120, align: 'right', cell: moneyCell },
  {
    type: 'actions',
    size: 100,
    maxInline: 2,
    actions: [
      { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`编辑 ${row.name}`) },
      { key: 'delete', buttonProps: { icon: 'i-lucide-trash-2', variant: 'ghost', color: 'error', size: 'xs' }, onClick: ({ row }) => notify(`删除 ${row.name}`) }
    ]
  }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    row-key="id"
    :columns="columns"
    :data="data"
    sortable
    bordered
  />
</template>
```

## Sample Data

All examples in this chapter derive their `data` from the following utility function. You can copy it directly into your project:

```ts [app/composables/useTableMock.ts]
export interface Person {
  id: string
  name: string
  email: string
  department: 'R&D' | 'Design' | 'Product' | 'Operations' | 'Marketing'
  role: string
  level?: 'P5' | 'P6' | 'P7' | 'P8'
  status: 'active' | 'leave' | 'offboarded'
  salary: number
  joinedAt: string
  address: string
  bio: string
  children?: Person[]
}

const FIRST = ['Zhang', 'Li', 'Wang', 'Chen', 'Liu', 'Yang', 'Huang', 'Zhao', 'Wu', 'Zhou', 'Xu', 'Sun']
const LAST = ['Wei', 'Fang', 'Na', 'Min', 'Jing', 'Li', 'Qiang', 'Lei', 'Jun', 'Yang', 'Yong', 'Tao', 'Ming', 'Chao']
const DEPARTMENTS: Person['department'][] = ['R&D', 'Design', 'Product', 'Operations', 'Marketing']
const ROLES = ['Frontend Engineer', 'Backend Engineer', 'Full Stack Engineer', 'UI Designer', 'Product Manager', 'Data Analyst', 'Operations Specialist']
const LEVELS: Person['level'][] = ['P5', 'P6', 'P7', 'P8']
const STATUS: Person['status'][] = ['active', 'leave', 'offboarded']
const ADDRESSES = [
  '708, IBM Tower, 2 Boyun Rd, Zhangjiang Hi-Tech Park, Pudong, Shanghai',
  '1502, Zhongguancun Tower A, 27 Zhongguancun St, Haidian, Beijing',
  '2301, Block B, Bay Technology Ecology Park, 9 Gaoxin Nan 9th Rd, Nanshan, Shenzhen',
  '38F, R&F Yingkai Plaza, 16 Huaxia Rd, Pearl River New Town, Tianhe, Guangzhou',
  '318, Building 6, Alibaba Xixi Campus, 969 Wenyi W Rd, Yuhang, Hangzhou',
  '1207, Building 5, Block D, Tianfu Software Park, 1199 Tianfu Ave, Hi-Tech Zone, Chengdu',
  '19F, Gaoke Rongyu Tower, 222 Jiangdong Zhong Rd, Jianye, Nanjing',
  '1808, Block B, Phase 3, Optics Valley International Enterprise Center, 70 Optics Valley Ave, Hongshan, Wuhan'
]
const BIO_TEMPLATES = [
  'Led supply chain and fulfillment infrastructure for a cross-border e-commerce team; spearheaded a real-time scheduling system overhaul handling millions of daily orders, with a focus on stability and cost efficiency.',
  'Long-term investment in big data platforms and offline computation scheduling; built a PB-scale data warehouse and governance framework with strong experience in data quality and access control.',
  'Focused on design systems and component libraries, driving visual consistency across products and leading multiple Design Token and dark-mode rollouts.',
  'Responsible for an in-house low-code platform and BFF middleware layer; delivered internal scaffolding and CLI tooling with an emphasis on developer productivity and maintainability.',
  'Deep expertise in real-time audio/video and WebRTC interactive scenarios; led latency optimization and weak-network degradation strategies for live rooms with tens of millions of concurrent users.',
  'Owned permissions, billing and multi-tenancy for a B2B SaaS product with full end-to-end experience in RBAC, ABAC and compliance auditing.',
  'Engineered risk control and anti-abuse algorithms; built real-time feature platforms and rules engines covering payment, marketing and social scenarios.',
  'Focused on mobile performance optimization and cross-platform rendering frameworks; led end-to-end governance of startup time, memory usage and bundle size.'
]

function pick<T>(list: readonly T[], seed: number): T {
  return list[seed % list.length] as T
}

export function makePerson(seed: number): Person {
  const first = pick(FIRST, seed)
  const last = pick(LAST, seed * 31 + 7)
  const dept = pick(DEPARTMENTS, seed * 7)
  return {
    id: `P${String(seed).padStart(4, '0')}`,
    name: `${first} ${last}`,
    email: `user${seed}@movk.dev`,
    department: dept,
    role: pick(ROLES, seed * 3),
    level: pick(LEVELS, seed * 11),
    status: pick(STATUS, seed * 13),
    salary: 8000 + (seed * 257) % 50000,
    joinedAt: new Date(2018 + (seed % 7), seed % 12, 1 + (seed % 27)).toISOString().slice(0, 10),
    address: pick(ADDRESSES, seed * 17),
    bio: pick(BIO_TEMPLATES, seed * 19)
  }
}

export function makePeople(count: number, offset = 0): Person[] {
  return Array.from({ length: count }, (_, i) => makePerson(offset + i + 1))
}

export function makePeopleTree(rootCount = 5, childPerRoot = 3, depth = 1): Person[] {
  const build = (seed: number, level: number): Person => {
    const person = makePerson(seed)
    if (level >= depth) return person
    return {
      ...person,
      role: 'Team Lead',
      level: undefined,
      children: Array.from({ length: childPerRoot }, (_, j) =>
        build(seed * 100 + j + 1, level + 1))
    }
  }
  return Array.from({ length: rootCount }, (_, i) => build(i + 1, 0))
}
```

## API

### Props

```ts
/**
 * Props for the MDataTable component
 */
interface MDataTableProps {
  /**
   * 行唯一标识字段，自动派生 getRowId；与 getRowId 同传时后者优先
   */
  rowKey?: string & {} | keyof T & string | undefined;
  columns?: DataTableColumn<T>[] | undefined;
  loading?: boolean | undefined;
  /**
   * 斑马纹
   * @default false
   */
  stripe?: boolean | undefined;
  /**
   * 纵向边框，传对象可定制 color/width/style
   * @default false
   */
  bordered?: boolean | { color?: string | undefined; width?: string | undefined; style?: "solid" | "dashed" | "dotted" | "double" | undefined; } | undefined;
  /**
   * 表格宽度由列宽内容决定（w-fit）
   * @default false
   */
  fitContent?: boolean | undefined;
  /**
   * 空值占位符
   * @default '-'
   */
  emptyCell?: string | false | (props: CellContext<T, unknown>): any | undefined;
  /**
   * 启用列固定按钮，传函数可按列动态决定
   * @default false
   */
  pinable?: boolean | ((col: DataTableDataColumn<T>) => boolean) | undefined;
  pinButtonProps?: ButtonProps | (ctx: DataTablePinButtonContext<T>): ButtonProps | undefined;
  /**
   * 启用列排序，传函数可按列动态决定
   * @default false
   */
  sortable?: boolean | ((col: DataTableDataColumn<T>) => boolean) | undefined;
  sortButtonProps?: ButtonProps | (ctx: DataTableSortButtonContext<T>): ButtonProps | undefined;
  /**
   * 全局 action 按钮 props，与列级 action.buttonProps 深度合并，列级优先
   */
  actionButtonProps?: ButtonProps | (ctx: DataTableActionButtonContext<T>): ButtonProps | undefined;
  /**
   * 行内最多展示多少 action 按钮，超出折叠到 overflow
   * @default 3
   */
  actionsMaxInline?: number | undefined;
  actionsOverflowTrigger?: ButtonProps | (ctx: CellContext<T, unknown>): ButtonProps | undefined;
  /**
   * 启用列宽拖拽，传函数可按列动态决定
   * @default false
   */
  resizable?: boolean | ((col: DataTableDataColumn<T>) => boolean) | undefined;
  /**
   * - 'onChange' 拖动中实时重排
   * - 'onEnd' 释放后才更新
   * @default 'onChange'
   */
  columnResizeMode?: "onChange" | "onEnd" | undefined;
  /**
   * 单元格内边距密度
   */
  density?: "compact" | "normal" | "comfortable" | { th?: string | ((cell: Header<T, unknown>) => string) | undefined; td?: string | ((cell: Cell<T, unknown>) => string) | undefined; } | undefined;
  meta?: TableMeta<T> | undefined;
  rowClass?: string | (row: T): string | undefined;
  rowStyle?: string | Record<string, string> | (row: T): string | Record<string, string> | undefined;
  /**
   * 单元格溢出 Tooltip：true 单行 / number 多行 / false 禁用 / 函数 动态
   * @default false
   */
  tooltip?: number | false | true | (ctx: CellContext<T, unknown>): number | boolean | undefined;
  tooltipProps?: OmitByKey<TooltipProps, "text"> | undefined;
  /**
   * 单元格文本截断：true 单行 / number 多行 / false 禁用 / 函数 动态
   * @default true
   */
  truncate?: number | false | true | (ctx: CellContext<T, unknown>): number | boolean | undefined;
  sortingOptions?: Omit<SortingOptions<T>, "getSortedRowModel" | "onSortingChange"> | undefined;
  columnSizingOptions?: Omit<ColumnSizingOptions, "onColumnSizingChange" | "onColumnSizingInfoChange"> | undefined;
  columnPinningOptions?: Omit<ColumnPinningOptions, "onColumnPinningChange"> | undefined;
  rowSelectionOptions?: Omit<RowSelectionOptions<T>, "onRowSelectionChange"> | undefined;
  /**
   * 子行字段名，设置后启用树形模式
   */
  childrenKey?: string & {} | keyof T & string | undefined;
  /**
   * 树形缩进：number 每层缩进 px / string CSS 值 / 函数 动态返回 CSS
   * @default '1rem'
   */
  indentSize?: string | number | (ctx: CellContext<T, unknown>): string | undefined;
  /**
   * 树形模式下的默认展开行为，仅在未提供 expanded / expandedKeys 时生效。true 展开全部父级行，number 展开 depth 小于该值的父级行，函数按行与深度自定义
   * @default false
   */
  defaultExpanded?: number | false | true | (row: T, depth: number): boolean | undefined;
  expandedOptions?: Omit<ExpandedOptions<T>, "getExpandedRowModel" | "onExpandedChange"> | undefined;
  /**
   * @default false
   */
  expandOnRowClick?: boolean | undefined;
  /**
   * @default false
   */
  selectOnRowClick?: boolean | undefined;
  /**
   * 可见列白名单（数组形）
   */
  columnVisibilityKeys?: string[] | undefined;
  /**
   * 隐藏列黑名单（数组形），与 columnVisibilityKeys 互斥，同传时白名单优先
   */
  columnVisibilityExcludeKeys?: string[] | undefined;
  /**
   * 选中行 id 列表（数组形）
   */
  rowSelectionKeys?: string[] | undefined;
  /**
   * 展开行 id 列表（数组形）
   */
  expandedKeys?: string[] | undefined;
  onSelect?: (e: Event, row: TableRow<T>): void | undefined;
  onHover?: (e: Event, row: TableRow<T> | null): void | undefined;
  onRowContextmenu?: (e: Event, row: TableRow<T>): void | ((e: Event, row: TableRow<T>) => void)[] | undefined;
  /**
   * 分页配置，透传给 TanStack / UTable
   * - 客户端分页：传入即启用，自动注入 getPaginationRowModel
   * - 服务端分页：manualPagination=true 并提供 rowCount 或 pageCount
   */
  paginationOptions?: Omit<PaginationOptions, "onPaginationChange"> | undefined;
  /**
   * 粘性表头
   * @default true
   */
  sticky?: boolean | "header" | "footer" | undefined;
  paginationUi?: DataTablePaginationUi & { ui?: { root?: SlotClass; summary?: SlotClass; summaryText?: SlotClass; selectedCount?: SlotClass; actions?: SlotClass; pageSizeSelect?: SlotClass; pagination?: SlotClass; } | undefined; } | undefined;
  /**
   * 触底加载回调，传入即启用无限滚动模式（自动隐藏内置分页、async 期间派生 loading）
   */
  loadMore?: (): void | Promise<void> | undefined;
  /**
   * 是否还能加载更多
   * @default true
   */
  canLoadMore?: boolean | undefined;
  /**
   * 触发 loadMore 的距底像素阈值
   * @default 100
   */
  loadMoreDistance?: number | undefined;
  ui?: Record<string, C> & { wrapper?: SlotClass; base?: SlotClass; tbody?: SlotClass; th?: SlotClass; td?: SlotClass; root?: SlotClass; caption?: SlotClass; thead?: SlotClass; tfoot?: SlotClass; tr?: SlotClass; separator?: SlotClass; empty?: SlotClass; loading?: SlotClass; } | undefined;
  /**
   * The element or component this component should render as.
   * @default 'div'
   */
  as?: any;
  data?: T[] | undefined;
  caption?: string | undefined;
  /**
   * Enable virtualization for large datasets.
   * Note: row pinning is not supported when virtualization is enabled.
   * @default false
   */
  virtualize?: boolean | (Partial<Omit<VirtualizerOptions<Element, Element>, "count" | "estimateSize" | "overscan">> & { getScrollElement?: (() => Element | null) | undefined; overscan?: number | undefined; estimateSize?: number | ((index: number) => number) | undefined; }) | undefined;
  /**
   * The text to display when the table is empty.
   * @default t('table.noData')
   */
  empty?: string | undefined;
  /**
   * @default 'primary'
   */
  loadingColor?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral" | undefined;
  /**
   * @default 'carousel'
   */
  loadingAnimation?: "carousel" | "carousel-inverse" | "swing" | "elastic" | undefined;
  /**
   * Use the `watchOptions` prop to customize reactivity (for ex: disable deep watching for changes in your data or limiting the max traversal depth). This can improve performance by reducing unnecessary re-renders, but it should be used with caution as it may lead to unexpected behavior if not managed properly.
   * @default { deep: true }
   */
  watchOptions?: WatchOptions<boolean> | undefined;
  globalFilterOptions?: Omit<GlobalFilterOptions<T>, "onGlobalFilterChange"> | undefined;
  columnFiltersOptions?: Omit<ColumnFiltersOptions<T>, "getFilteredRowModel" | "onColumnFiltersChange"> | undefined;
  visibilityOptions?: Omit<VisibilityOptions, "onColumnVisibilityChange"> | undefined;
  groupingOptions?: Omit<GroupingOptions, "onGroupingChange"> | undefined;
  rowPinningOptions?: Omit<RowPinningOptions<T>, "onRowPinningChange"> | undefined;
  facetedOptions?: FacetedOptions<T> | undefined;
  state?: Partial<TableState> | undefined;
  onStateChange?: (updater: Updater<TableState>): void | undefined;
  renderFallbackValue?: any;
  /**
   * An array of extra features that you can add to the table instance.
   */
  _features?: TableFeature<any>[] | undefined;
  /**
   * Set this option to override any of the `autoReset...` feature options.
   */
  autoResetAll?: boolean | undefined;
  /**
   * Set this option to `true` to output all debugging information to the console.
   */
  debugAll?: boolean | undefined;
  /**
   * Set this option to `true` to output cell debugging information to the console.
   */
  debugCells?: boolean | undefined;
  /**
   * Set this option to `true` to output column debugging information to the console.
   */
  debugColumns?: boolean | undefined;
  /**
   * Set this option to `true` to output header debugging information to the console.
   */
  debugHeaders?: boolean | undefined;
  /**
   * Set this option to `true` to output row debugging information to the console.
   */
  debugRows?: boolean | undefined;
  /**
   * Set this option to `true` to output table debugging information to the console.
   */
  debugTable?: boolean | undefined;
  /**
   * Default column options to use for all column defs supplied to the table.
   */
  defaultColumn?: Partial<ColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<ColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | Partial<GroupColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<GroupColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | Partial<AccessorKeyColumnDefBase<T, unknown> & Partial<StringHeaderIdentifier>> | Partial<AccessorKeyColumnDefBase<T, unknown> & Partial<IdIdentifier<T, unknown>>> | Partial<AccessorFnColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<AccessorFnColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | undefined;
  /**
   * This optional function is used to derive a unique ID for any given row. If not provided the rows index is used (nested rows join together with `.` using their grandparents' index eg. `index.index.index`). If you need to identify individual rows that are originating from any server-side operations, it's suggested you use this function to return an ID that makes sense regardless of network IO/ambiguity eg. a userId, taskId, database ID field, etc.
   */
  getRowId?: (originalRow: T, index: number, parent?: Row<T> | undefined): string | undefined;
  /**
   * This optional function is used to access the sub rows for any given row. If you are using nested rows, you will need to use this function to return the sub rows object (or undefined) from the row.
   */
  getSubRows?: (originalRow: T, index: number): T[] | undefined | undefined;
  /**
   * Use this option to optionally pass initial state to the table. This state will be used when resetting various table states either automatically by the table (eg. `options.autoResetPageIndex`) or via functions like `table.resetRowSelection()`. Most reset function allow you optionally pass a flag to reset to a blank/default state instead of the initial state.
   * 
   * Table state will not be reset when this object changes, which also means that the initial state object does not need to be stable.
   */
  initialState?: InitialTableState | undefined;
  /**
   * This option is used to optionally implement the merging of table options.
   */
  mergeOptions?: (defaultOptions: TableOptions<T>, options: Partial<TableOptions<T>>): TableOptions<T> | undefined;
  cellpadding?: string | number | undefined;
  cellspacing?: string | number | undefined;
  summary?: string | undefined;
  width?: string | number | undefined;
  pagination?: PaginationState | undefined;
  columnVisibility?: VisibilityState | undefined;
  columnPinning?: ColumnPinningState | undefined;
  columnSizing?: ColumnSizingState | undefined;
  rowSelection?: RowSelectionState | undefined;
  /**
   * @default { top: [], bottom: [] }
   */
  rowPinning?: RowPinningState | undefined;
  /**
   * @default []
   */
  sorting?: SortingState | undefined;
  expanded?: true | Record<string, boolean> | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the MDataTable component
 */
interface MDataTableEmits {
  update:pagination: (payload: [value: PaginationState | undefined]) => void;
  update:columnVisibility: (payload: [value: VisibilityState | undefined]) => void;
  update:columnPinning: (payload: [value: ColumnPinningState | undefined]) => void;
  update:columnSizing: (payload: [value: ColumnSizingState | undefined]) => void;
  update:rowSelection: (payload: [value: RowSelectionState | undefined]) => void;
  update:rowPinning: (payload: [value: RowPinningState]) => void;
  update:sorting: (payload: [value: SortingState]) => void;
  update:expanded: (payload: [value: ExpandedState | undefined]) => void;
  update:columnVisibilityKeys: (payload: [value: string[] | undefined]) => void;
  update:columnVisibilityExcludeKeys: (payload: [value: string[] | undefined]) => void;
  update:rowSelectionKeys: (payload: [value: string[] | undefined]) => void;
  update:expandedKeys: (payload: [value: string[] | undefined]) => void;
}
```

### Slots

```ts
/**
 * Slots for the MDataTable component
 */
interface MDataTableSlots {
  expanded(): any;
  empty(): any;
  loading(): any;
  caption(): any;
  body-top(): any;
  body-bottom(): any;
  pagination(): any;
  pagination-summary(): any;
  pagination-actions(): any;
}
```

Besides the named slots above, dynamic slots named after column ids are supported: `#<column>-header`, `#<column>-cell` and `#<column>-footer`. See [Columns · Header and Cell Slots](https://nuxt.mhaibaraai.cn/docs/data-table/columns#header-and-cell-slots).

### Expose

Access the typed component instance (`DataTableExposed<T>`) via [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref).

| Name | Type |
| --- | --- |
| `tableRef` | `HTMLTableElement \| null` <br> Native table element |
| `tableApi` | `Table<T> \| null` <br> TanStack Table instance |
| `el` | `HTMLElement \| null` <br> UTable root element (scroll container) |
| `scrollToTop(options?)` | `void` <br> Scroll to top |
| `clearSelection()` | `void` <br> Clear row selection |


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
