Introduction

View source
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 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.

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 togglessortable, 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:

姓名
#
部门
岗位
状态
薪资
操作
李勇
1
产品
UI 设计师
休假
¥8,257
王超
2
市场
运营专员
已离职
¥8,514
陈娜
3
设计
全栈工程师
在职
¥8,771
刘丽
4
运营
数据分析师
休假
¥9,028
杨军
5
研发
后端工程师
已离职
¥9,285
黄涛
6
产品
产品经理
在职
¥9,542
<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:

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

Prop Default Type
as'div'any

The element or component this component should render as.

rowKeystring & {} | keyof T & string

行唯一标识字段,自动派生 getRowId;与 getRowId 同传时后者优先

columnsDataTableColumn<T>[]
borderedfalseboolean | { color?: string; width?: string; style?: "solid" | "dashed" | "dotted" | "double"; }

纵向边框,传对象可定制 color/width/style

emptyCell'-'string | false | (props: CellContext<T, unknown>): any

空值占位符

pinablefalseboolean | ((col: DataTableDataColumn<T>) => boolean)

启用列固定按钮,传函数可按列动态决定

pinButtonPropsButtonProps | (ctx: DataTablePinButtonContext<T>): ButtonProps
sortablefalseboolean | ((col: DataTableDataColumn<T>) => boolean)

启用列排序,传函数可按列动态决定

sortButtonPropsButtonProps | (ctx: DataTableSortButtonContext<T>): ButtonProps
actionButtonPropsButtonProps | (ctx: DataTableActionButtonContext<T>): ButtonProps

全局 action 按钮 props,与列级 action.buttonProps 深度合并,列级优先

actionsMaxInline3number

行内最多展示多少 action 按钮,超出折叠到 overflow

actionsOverflowTriggerButtonProps | (ctx: CellContext<T, unknown>): ButtonProps
resizablefalseboolean | ((col: DataTableDataColumn<T>) => boolean)

启用列宽拖拽,传函数可按列动态决定

columnResizeMode'onChange'"onChange" | "onEnd"
  • 'onChange' 拖动中实时重排
  • 'onEnd' 释放后才更新
density"compact" | "normal" | "comfortable" | { th?: string | ((cell: Header<T, unknown>) => string); td?: string | ((cell: Cell<T, unknown>) => string); }

单元格内边距密度

metaTableMeta<T>
rowClassstring | (row: T): string
rowStylestring | Record<string, string> | (row: T): string | Record<string, string>
tooltipfalsenumber | false | true | (ctx: CellContext<T, unknown>): number | boolean

单元格溢出 Tooltip:true 单行 / number 多行 / false 禁用 / 函数 动态

tooltipPropsOmitByKey<TooltipProps, "text">
truncatetruenumber | false | true | (ctx: CellContext<T, unknown>): number | boolean

单元格文本截断:true 单行 / number 多行 / false 禁用 / 函数 动态

sortingOptionsOmit<SortingOptions<T>, "getSortedRowModel" | "onSortingChange">
columnSizingOptionsOmit<ColumnSizingOptions, "onColumnSizingChange" | "onColumnSizingInfoChange">
columnPinningOptionsOmit<ColumnPinningOptions, "onColumnPinningChange">
rowSelectionOptionsOmit<RowSelectionOptions<T>, "onRowSelectionChange">
childrenKeystring & {} | keyof T & string

子行字段名,设置后启用树形模式

indentSize'1rem'string | number | (ctx: CellContext<T, unknown>): string

树形缩进:number 每层缩进 px / string CSS 值 / 函数 动态返回 CSS

defaultExpandedfalsenumber | false | true | (row: T, depth: number): boolean

树形模式下的默认展开行为,仅在未提供 expanded / expandedKeys 时生效。true 展开全部父级行,number 展开 depth 小于该值的父级行,函数按行与深度自定义

expandedOptionsOmit<ExpandedOptions<T>, "getExpandedRowModel" | "onExpandedChange">
columnVisibilityKeysstring[]

可见列白名单(数组形)

columnVisibilityExcludeKeysstring[]

隐藏列黑名单(数组形),与 columnVisibilityKeys 互斥,同传时白名单优先

rowSelectionKeysstring[]

选中行 id 列表(数组形)

expandedKeysstring[]

展开行 id 列表(数组形)

onSelectDataTableSelectHandler<T>
onHoverDataTableHoverHandler<T>
onRowContextmenu(e: Event, row: TableRow<T>): void | ((e: Event, row: TableRow<T>) => void)[]
paginationOptionsOmit<PaginationOptions, "onPaginationChange">

分页配置,透传给 TanStack / UTable

  • 客户端分页:传入即启用,自动注入 getPaginationRowModel
  • 服务端分页:manualPagination=true 并提供 rowCount 或 pageCount
stickytrueboolean | "header" | "footer"

粘性表头

paginationUiDataTablePaginationUi & { ui?: { root?: SlotClass; summary?: SlotClass; summaryText?: SlotClass; selectedCount?: SlotClass; actions?: SlotClass; pageSizeSelect?: SlotClass; pagination?: SlotClass; }; }
loadMore() => void | Promise<void>

触底加载回调,传入即启用无限滚动模式(自动隐藏内置分页、async 期间派生 loading)

loadMoreDistance100number

触发 loadMore 的距底像素阈值

dataT[]
captionstring
virtualizefalseboolean | (Partial<Omit<VirtualizerOptions<Element, Element>, "count" | "getScrollElement" | "estimateSize" | "overscan">> & { overscan?: number; estimateSize?: number | ((index: number) => number); })

Enable virtualization for large datasets. Note: row pinning is not supported when virtualization is enabled.

emptyt('table.noData')string

The text to display when the table is empty.

loadingColor'primary'"primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral"
loadingAnimation'carousel'"carousel" | "carousel-inverse" | "swing" | "elastic"
watchOptions{ deep: true }WatchOptions<boolean>

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.

globalFilterOptionsOmit<GlobalFilterOptions<T>, "onGlobalFilterChange">
columnFiltersOptionsOmit<ColumnFiltersOptions<T>, "getFilteredRowModel" | "onColumnFiltersChange">
visibilityOptionsOmit<VisibilityOptions, "onColumnVisibilityChange">
groupingOptionsOmit<GroupingOptions, "onGroupingChange">
rowPinningOptionsOmit<RowPinningOptions<T>, "onRowPinningChange">
facetedOptionsFacetedOptions<T>
statePartial<TableState>
onStateChange(updater: Updater<TableState>) => void
renderFallbackValueany
_featuresTableFeature<any>[]

An array of extra features that you can add to the table instance.

defaultColumnPartial<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>>

Default column options to use for all column defs supplied to the table.

getRowId(originalRow: T, index: number, parent?: Row<T>) => string

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.

getSubRows(originalRow: T, index: number) => T[]

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.

initialStateInitialTableState

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.

mergeOptions(defaultOptions: TableOptions<T>, options: Partial<TableOptions<T>>) => TableOptions<T>

This option is used to optionally implement the merging of table options.

cellpaddingstring | number
cellspacingstring | number
summarystring
widthstring | number
paginationPaginationState
columnVisibilityVisibilityState
columnPinningColumnPinningState
columnSizingColumnSizingState
rowSelectionRowSelectionState
rowPinning{ top: [], bottom: [] }RowPinningState
sorting[]SortingState
expandedtrue | Record<string, boolean>
loadingboolean
stripefalseboolean

斑马纹

fitContentfalseboolean

表格宽度由列宽内容决定(w-fit)

expandOnRowClickfalseboolean
selectOnRowClickfalseboolean
canLoadMoretrueboolean

是否还能加载更多

autoResetAllboolean

Set this option to override any of the autoReset... feature options.

debugAllboolean

Set this option to true to output all debugging information to the console.

debugCellsboolean

Set this option to true to output cell debugging information to the console.

debugColumnsboolean

Set this option to true to output column debugging information to the console.

debugHeadersboolean

Set this option to true to output header debugging information to the console.

debugRowsboolean

Set this option to true to output row debugging information to the console.

debugTableboolean

Set this option to true to output table debugging information to the console.

uiRecord<string, ClassNameValue> & { 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; }

Emits

Event Type
update:pagination[value: PaginationState]
update:columnVisibility[value: VisibilityState]
update:columnPinning[value: ColumnPinningState]
update:columnSizing[value: ColumnSizingState]
update:rowSelection[value: RowSelectionState]
update:rowPinning[value: RowPinningState]
update:sorting[value: SortingState]
update:expanded[value: ExpandedState]
update:columnVisibilityKeys[value: string[]]
update:columnVisibilityExcludeKeys[value: string[]]
update:rowSelectionKeys[value: string[]]
update:expandedKeys[value: string[]]

Slots

Slot Type
expanded{ row: Row<T>; }
emptyany
loadingany
captionany
body-topany
body-bottomany
paginationDataTablePaginationSlotProps<T>
pagination-summary{ summaryText: string; selectedText: string; selectedCount: number; rowCount: number; rowCountKnown: boolean; from: number; to: number; page: number; pageCount: number; showSelectedCount: boolean; }
pagination-actions{ tableApi: Table<T>; page: number; pageCount: number; pageSize: number; rowCount: number; pageSizes: number[]; pageSizeOptions: { label: string; value: number; }[]; showPageSizeSelect: boolean; setPage: (page: number) => void; setPageSize: (pageSize: unknown) => void; }

Expose

Access the typed component instance (DataTableExposed<T>) via useTemplateRef.

NameType
tableRefHTMLTableElement | null

Native table element

tableApiTable<T> | null

TanStack Table instance

elHTMLElement | null

UTable root element (scroll container)

scrollToTop(options?)void

Scroll to top

clearSelection()void

Clear row selection

Copyright © 2025 - 2026 YiXuan - MIT License