Rows & Interaction

View source
Row selection, expandable detail slot, click behaviors, dynamic row class/style, hover, context menu and imperative control.

rowKey Row Identifier

row-key specifies the unique identifier field for a row and automatically derives TanStack's getRowId. It is the foundation for selection, expansion, tree data and other features to correctly track rows. Once set, selection and expansion state are tracked by the business id rather than the array index — reordering data keeps the selected rows pointing to the same records:

工号姓名部门
P0001
李勇
产品
P0002
王超
市场
P0003
陈娜
设计
P0004
刘丽
运营
P0005
杨军
研发
P0006
黄涛
产品
selection: {}
<script setup lang="ts">
import type { DataTableColumn, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const rows = ref<Person[]>(makePeople(6))
const selection = ref<RowSelectionState>({})

const columns: DataTableColumn<Person>[] = [
  { type: 'selection' },
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

function shuffle(): void {
  rows.value = [...rows.value].sort(() => Math.random() - 0.5)
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div>
      <UButton size="xs" variant="soft" icon="i-lucide-shuffle" @click="shuffle">
        打乱数据
      </UButton>
    </div>
    <MDataTable v-model:row-selection="selection" row-key="id" :columns="columns" :data="rows" />
    <pre class="text-xs p-3 rounded-md bg-muted overflow-auto">selection: {{ selection }}</pre>
  </div>
</template>

Basic Row Selection

v-model:row-selection bi-directionally syncs the selected id set; the @select event returns the currently clicked row (DataTableSelectHandler):

姓名部门岗位
李勇
产品
UI 设计师
王超
市场
运营专员
陈娜
设计
全栈工程师
刘丽
运营
数据分析师
杨军
研发
后端工程师
黄涛
产品
产品经理
赵伟
市场
前端工程师
吴敏
设计
UI 设计师
周强
运营
运营专员
徐洋
研发
全栈工程师

最近点击:(无)

selection: {}
<script setup lang="ts">
import type { DataTableColumn, DataTableSelectHandler, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(10)
const selection = ref<RowSelectionState>({})
const lastSelected = ref('')

const columns: DataTableColumn<Person>[] = [
  { type: 'selection' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]

const onSelect: DataTableSelectHandler<Person> = (_e, row) => {
  lastSelected.value = row.original.name
}
</script>

<template>
  <div class="flex flex-col gap-2">
    <MDataTable
      v-model:row-selection="selection"
      row-key="id"
      :columns="columns"
      :data="data"
      :ui="{ root: 'max-h-[50vh]' }"
      @select="onSelect"
    />
    <p class="text-xs text-muted">
      最近点击:{{ lastSelected || '(无)' }}
    </p>
    <pre class="text-xs p-3 rounded-md bg-muted overflow-auto">selection: {{ selection }}</pre>
  </div>
</template>

Row Expansion and Detail Slot

v-model:expanded controls expanded row ids; the #expanded slot renders the detail area:

The type: 'expand' column header renders an "expand all / collapse all" button by default — disable it with toggleAll: false
姓名部门岗位
李勇
产品
UI 设计师
王超
市场
运营专员
陈娜
设计
全栈工程师
刘丽
运营
数据分析师
杨军
研发
后端工程师
黄涛
产品
产品经理
赵伟
市场
前端工程师
吴敏
设计
UI 设计师
<script setup lang="ts">
import type { DataTableColumn, ExpandedState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const expanded = ref<ExpandedState>({})

const columns: DataTableColumn<Person>[] = [
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:expanded="expanded"
    row-key="id"
    :columns="columns"
    :data="data"
    :ui="{ root: 'max-h-[50vh]' }"
  >
    <template #expanded="{ row }">
      <div class="px-4 py-3 text-sm bg-elevated/30">
        <p>邮箱 {{ row.original.email }} · 入职 {{ row.original.joinedAt }}</p>
        <p class="text-muted">
          {{ row.original.bio }}
        </p>
      </div>
    </template>
  </MDataTable>
</template>

Row Click for Selection and Expansion

select-on-row-click toggles selection on a full-row click; expand-on-row-click toggles expansion on a full-row click. Both can be enabled independently:

姓名部门
李勇
产品
王超
市场
陈娜
设计
刘丽
运营
杨军
研发
黄涛
产品
赵伟
市场
吴敏
设计
<script setup lang="ts">
import type { DataTableColumn, ExpandedState, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const selection = ref<RowSelectionState>({})
const expanded = ref<ExpandedState>({})

const columns: DataTableColumn<Person>[] = [
  { type: 'selection' },
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]
</script>

<template>
  <MDataTable
    v-model:row-selection="selection"
    v-model:expanded="expanded"
    row-key="id"
    :columns="columns"
    :data="data"
    :ui="{ root: 'max-h-[50vh]' }"
  >
    <template #expanded="{ row }">
      <div class="px-4 py-2 text-xs text-muted bg-elevated/30">
        {{ row.original.bio }}
      </div>
    </template>
  </MDataTable>
</template>

Dynamic Row Styles

row-class accepts (row) => string; row-style accepts (row) => string | Record<string, string> to derive class names and inline styles from row data:

姓名状态薪资
李勇
leave
8257
王超
offboarded
8514
陈娜
active
8771
刘丽
leave
9028
杨军
offboarded
9285
黄涛
active
9542
赵伟
leave
9799
吴敏
offboarded
10056
周强
active
10313
徐洋
leave
10570
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(10)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' },
  { accessorKey: 'salary', header: '薪资', align: 'right', size: 110 }
]

function rowClassByStatus(row: Person): string {
  if (row.status === 'offboarded') return 'bg-gray-50/40 dark:bg-gray-900/10'
  if (row.status === 'leave') return 'bg-warning-50/40 dark:bg-warning-900/10'
  return ''
}
function rowStyleBySalary(row: Person): Record<string, string> {
  if (row.salary >= 40000) return { fontWeight: '600', color: 'var(--ui-color-error-600, #dc2626)' }
  if (row.salary >= 25000) return { fontWeight: '500' }
  return {}
}
</script>

<template>
  <MDataTable
    row-key="id"
    :columns="columns"
    :data="data"
    :row-class="rowClassByStatus"
    :row-style="rowStyleBySalary"
    :ui="{ root: 'max-h-[50vh]' }"
  />
</template>

Row Hover Tracking

@hover fires on row enter and leave (DataTableHoverHandler); the second argument is a TableRow or null, useful for syncing external highlights:

悬停行:(无)

姓名部门岗位
李勇
产品
UI 设计师
王超
市场
运营专员
陈娜
设计
全栈工程师
刘丽
运营
数据分析师
杨军
研发
后端工程师
黄涛
产品
产品经理
赵伟
市场
前端工程师
吴敏
设计
UI 设计师
<script setup lang="ts">
import type { DataTableColumn, DataTableHoverHandler } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const hovered = ref<string | null>(null)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]

const onHover: DataTableHoverHandler<Person> = (_e, row) => {
  hovered.value = row ? row.original.name : null
}
</script>

<template>
  <div class="flex flex-col gap-2">
    <p class="text-xs text-muted">
      悬停行:{{ hovered ?? '(无)' }}
    </p>
    <MDataTable row-key="id" :columns="columns" :data="data" :ui="{ root: 'max-h-[50vh]' }" @hover="onHover" />
  </div>
</template>

Row Context Menu

@row-contextmenu intercepts right-click events (DataTableContextmenuHandler); the callback receives the original MouseEvent and the target row, allowing you to prevent the default menu and show a custom one:

姓名部门岗位
李勇
产品
UI 设计师
王超
市场
运营专员
陈娜
设计
全栈工程师
刘丽
运营
数据分析师
杨军
研发
后端工程师
黄涛
产品
产品经理
赵伟
市场
前端工程师
吴敏
设计
UI 设计师
<script setup lang="ts">
import type { DataTableColumn, DataTableContextmenuHandler } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const toast = useToast()

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]

const onRowContextmenu: DataTableContextmenuHandler<Person> = (e, row) => {
  e.preventDefault()
  toast.add({ title: `右键 ${row.original.name}`, duration: 1500 })
}
</script>

<template>
  <MDataTable
    row-key="id"
    :columns="columns"
    :data="data"
    :ui="{ root: 'max-h-[50vh]' }"
    @row-contextmenu="onRowContextmenu"
  />
</template>

Imperative Control

Obtain the component instance via useTemplateRef and call the exposed clearSelection() to clear selection or scrollToTop() to scroll back to the top:

姓名部门岗位
李勇
产品
UI 设计师
王超
市场
运营专员
陈娜
设计
全栈工程师
刘丽
运营
数据分析师
杨军
研发
后端工程师
黄涛
产品
产品经理
赵伟
市场
前端工程师
吴敏
设计
UI 设计师
周强
运营
运营专员
徐洋
研发
全栈工程师
孙明
产品
数据分析师
张芳
市场
后端工程师
李静
设计
产品经理
王磊
运营
前端工程师
陈勇
研发
UI 设计师
<script setup lang="ts">
import type { DataTableColumn, DataTableExposed, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(15)
const tableRef = useTemplateRef<DataTableExposed<Person>>('tableRef')
const selection = ref<RowSelectionState>({})

const columns: DataTableColumn<Person>[] = [
  { type: 'selection' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex flex-wrap gap-2">
      <UButton size="xs" variant="soft" icon="i-lucide-eraser" @click="tableRef?.clearSelection()">
        清空选中
      </UButton>
      <UButton size="xs" variant="soft" icon="i-lucide-arrow-up-to-line" @click="tableRef?.scrollToTop({ behavior: 'smooth' })">
        回到顶部
      </UButton>
    </div>
    <MDataTable
      ref="tableRef"
      v-model:row-selection="selection"
      row-key="id"
      :columns="columns"
      :data="data"
      select-on-row-click
      :ui="{ root: 'max-h-[40vh]' }"
    />
  </div>
</template>
Copyright © 2025 - 2026 YiXuan - MIT License