---
title: "Special Columns"
description: "Configuration for selection, index, row-pinning and actions columns — including tree selection strategies, dynamic button props and confirmation dialogs."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/data-table/special-columns"
---
# Special Columns

> Configuration for selection, index, row-pinning and actions columns — including tree selection strategies, dynamic button props and confirmation dialogs.

## Overview

Data columns bind fields via `accessorKey`; special columns are distinguished by `type` and do not need an `accessorKey`:

| `type` | Purpose |
| --- | --- |
| `selection` | Row selection (single/multiple, tree cascade strategies) |
| `index` | Row index |
| `expand` | Tree expansion (see [Tree Data](https://nuxt.mhaibaraai.cn/docs/data-table/tree)) |
| `row-pinning` | Pin rows to top or bottom |
| `actions` | Row action buttons |

> [!TIP]
> 
> All special columns also support 
> 
> visibility
> 
> , 
> 
> pinable
> 
> , 
> 
> resizable
> 
> , 
> 
> fixed
> 
> , 
> 
> size
> 
> , 
> 
> align
> 
> .

## Selection Column

### `mode` Single / Multiple

How `mode` works: the default `multiple` automatically renders header and cell checkboxes and syncs the selected-id dictionary via `v-model:row-selection`; `single` disables multi-select, shows a custom `header`, and keeps at most one selected row at all times:

```vue [DataTableSelectionExample.vue]
<script setup lang="ts">
import type { DataTableColumn, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const props = defineProps<{
  mode: 'multiple' | 'single'
}>()

const data = makePeople(6)
const selection = ref<RowSelectionState>({})

const columns = computed<DataTableColumn<Person>[]>(() => [
  props.mode === 'single'
    ? { type: 'selection', mode: 'single', header: '选择', size: 80 }
    : { type: 'selection' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
])
</script>

<template>
  <div class="flex flex-col gap-3">
    <MDataTable v-model:row-selection="selection" row-key="id" :columns="columns" :data="data" />
    <pre class="text-xs p-3 rounded-md bg-muted overflow-auto">rowSelection: {{ selection }}</pre>
  </div>
</template>
```

### `strategy` Tree Selection Strategy

`strategy` takes effect in tree mode (`children-key`) and controls the parent–child check relationship: `cascade` for parent–child cascade, `isolated` for independent selection, `leaf` for leaf-only selection (parent nodes show a derived state):

```vue [DataTableSelectionStrategyExample.vue]
<script setup lang="ts">
import type { DataTableColumn, DataTableTreeSelectionStrategy, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const treeData = makePeopleTree(3, 3)
const cascade = ref<RowSelectionState>({})
const isolated = ref<RowSelectionState>({})
const leaf = ref<RowSelectionState>({})

const treeBase: DataTableColumn<Person>[] = [
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]
function strategyColumns(strategy: DataTableTreeSelectionStrategy): DataTableColumn<Person>[] {
  return [{ type: 'selection', strategy }, ...treeBase]
}
</script>

<template>
  <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
    <div class="flex flex-col gap-2">
      <div class="text-xs text-muted">
        cascade（父子级联）
      </div>
      <MDataTable
        v-model:row-selection="cascade"
        row-key="id"
        children-key="children"
        :columns="strategyColumns('cascade')"
        :data="treeData"
      />
    </div>
    <div class="flex flex-col gap-2">
      <div class="text-xs text-muted">
        isolated（独立勾选）
      </div>
      <MDataTable
        v-model:row-selection="isolated"
        row-key="id"
        children-key="children"
        :columns="strategyColumns('isolated')"
        :data="treeData"
      />
    </div>
    <div class="flex flex-col gap-2">
      <div class="text-xs text-muted">
        leaf（仅叶子）
      </div>
      <MDataTable
        v-model:row-selection="leaf"
        row-key="id"
        children-key="children"
        :columns="strategyColumns('leaf')"
        :data="treeData"
      />
    </div>
  </div>
</template>
```

### `checkboxProps` Functional Configuration

`checkboxProps` receives context and returns props: you can distinguish header/cell by `scope`, parent row derived state by `isLeafAggregate`, and derive `disabled` from row data:

```vue [DataTableSelectionCheckboxPropsExample.vue]
<script setup lang="ts">
import type { DataTableColumn, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const treeData = makePeopleTree(3, 3)
const selection = ref<RowSelectionState>({
  P0101: true,
  P0102: true,
  P0201: true,
  P0202: true,
  P0203: true
})

const columns: DataTableColumn<Person>[] = [
  {
    type: 'selection',
    strategy: 'leaf',
    checkboxProps: (ctx) => {
      if (ctx.scope === 'header') return { color: 'success' }
      if (ctx.isLeafAggregate) return { color: 'neutral' }
      return {
        disabled: ctx.cellContext.row.original.status === 'offboarded',
        color: 'primary'
      }
    }
  },
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' }
]
</script>

<template>
  <MDataTable
    v-model:row-selection="selection"
    row-key="id"
    children-key="children"
    :columns="columns"
    :data="treeData"
  />
</template>
```

### rowSelectionKeys and treeSelection

`v-model:row-selection-keys` provides external controlled state in array form; the component-exposed `treeSelection` derives business views such as `leaves`, `parents`, `halfSelected` and `strictlyChecked` (accessible via `useTemplateRef`):

```vue [DataTableSelectionKeysExample.vue]
<script setup lang="ts">
import type { DataTableColumn, DataTableExposed } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const treeData = makePeopleTree(3, 3)
const selectionKeys = ref<string[]>([])
const tableRef = useTemplateRef<DataTableExposed<Person>>('tableRef')

const derived = computed(() => {
  const r = tableRef.value?.treeSelection
  return {
    leaves: r?.leaves.map(p => p.name) ?? [],
    parents: r?.parents.map(p => p.name) ?? [],
    halfSelected: r?.halfSelected.map(p => p.name) ?? []
  }
})

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

<template>
  <div class="flex flex-col gap-3">
    <MDataTable
      ref="tableRef"
      v-model:row-selection-keys="selectionKeys"
      row-key="id"
      children-key="children"
      :columns="columns"
      :data="treeData"
    />
    <pre class="text-xs p-3 rounded-md bg-muted overflow-auto">treeSelection: {{ derived }}</pre>
  </div>
</template>
```

## Index Column

`type: 'index'` renders a `#` header by default; the row number is automatically recalculated with sorting and pagination, and can be customized via `header`:

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

const data = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })

const columns: DataTableColumn<Person>[] = [
  { type: 'index', header: '序' },
  { accessorKey: 'name', header: '姓名', sortable: true },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'salary', header: '薪资', align: 'right', sortable: true }
]
</script>

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

## Row Pinning Column

### `position` Pin to Top / Bottom

The `position` prop of `type: 'row-pinning'` takes `top` or `bottom`, pinning rows to the header or footer. `v-model:row-pinning` exposes two id groups: `top` and `bottom`:

```vue [DataTableRowPinningExample.vue]
<script setup lang="ts">
import type { DataTableColumn, RowPinningState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const pinning = ref<RowPinningState>({ top: ['P0001'], bottom: ['P0004', 'P0008'] })

const columns: DataTableColumn<Person>[] = [
  { type: 'row-pinning', position: 'top', header: '顶' },
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable v-model:row-pinning="pinning" row-key="id" :columns="columns" :data="data" :ui="{ root: 'max-h-65' }" />
</template>
```

### `buttonProps` Dynamic Pin Button

`buttonProps` receives the `pinned` state and returns `icon`, `color` and `variant`, distinguishing pinned from unpinned rows:

```vue [DataTableRowPinningDynamicExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(6)

const columns: DataTableColumn<Person>[] = [
  {
    type: 'row-pinning',
    buttonProps: ({ pinned }) => pinned
      ? { icon: 'i-lucide-pin-off', color: 'primary', variant: 'outline' }
      : { icon: 'i-lucide-pin', color: 'neutral', variant: 'ghost' }
  },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```

## Actions Column

### `maxInline` Inline and Overflow

`maxInline` controls the number of inline buttons; extras collapse into an overflow menu. `action.divider` inserts a separator in the menu:

```vue [DataTableActionsInlineExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  {
    type: 'actions',
    minSize: 80,
    maxInline: 2,
    actions: [
      { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
      { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`编辑 ${row.name}`) },
      { key: 'archive', buttonProps: { icon: 'i-lucide-archive', label: '归档', size: 'xs' }, onClick: ({ row }) => notify(`归档 ${row.name}`) },
      { key: 'delete', divider: true, buttonProps: { icon: 'i-lucide-trash-2', label: '删除', color: 'error', size: 'xs' }, onClick: ({ row }) => notify(`删除 ${row.name}`) }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```

### `actions` Array and Functional

`actions` accepts either a static array or a `(ctx) => DataTableAction[]` function to derive actions per row:

```vue [DataTableActionsFunctionalExample.vue]
<script setup lang="ts">
import type { DataTableAction, DataTableActionsColumn, DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const base: DataTableAction<Person>[] = [
  { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
  { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`编辑 ${row.name}`) }
]

const actions: DataTableActionsColumn<Person>['actions'] = ({ row: { original } }) => {
  const extras: DataTableAction<Person>[] = []
  if (original.department === '研发')
    extras.push({ key: 'resetPwd', buttonProps: { icon: 'i-lucide-key-round', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`重置密码 ${row.name}`) })
  if (original.department === '设计')
    extras.push({ key: 'transfer', buttonProps: { icon: 'i-lucide-share-2', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`转交 ${row.name}`) })
  return [...base, ...extras]
}

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { type: 'actions', maxInline: 4, actions, size: 120 }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```

### Async Action Auto Loading

When `onClick` returns a Promise, the button automatically enters a loading state and is disabled to prevent re-triggering (inline buttons only):

```vue [DataTableActionsAsyncExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  {
    type: 'actions',
    maxInline: 2,
    size: 120,
    actions: [
      { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
      {
        key: 'sync',
        buttonProps: { icon: 'i-lucide-refresh-cw', label: '同步', size: 'xs' },
        onClick: async ({ row }) => {
          await new Promise(r => setTimeout(r, 1200))
          notify(`同步完成 ${row.name}`)
        }
      }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```

### `confirm` Confirmation Dialog

`confirm: true` pops up a MessageBox before executing; `confirmProps` can dynamically return `title`, `type`, `description` and `confirmText`:

```vue [DataTableActionsConfirmExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' },
  {
    type: 'actions',
    size: 120,
    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', label: '删除', color: 'error', size: 'xs' },
        confirm: true,
        confirmProps: ({ row }) => ({
          type: row.status === 'active' ? 'warning' : 'error',
          title: `删除 ${row.name}？`,
          description: row.status === 'active' ? '该员工在职，删除前请二次确认。' : '已离职员工删除后无法恢复。',
          confirmText: '我已确认'
        }),
        onClick: ({ row }) => notify(`已删除 ${row.name}`)
      }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```

### Dynamic `disabled` and `visibility`

`action.disabled` and `action.visibility` accept a boolean or `(ctx) => boolean` to derive disabled or hidden state per row:

```vue [DataTableActionsDynamicStateExample.vue]
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' },
  {
    type: 'actions',
    maxInline: 3,
    size: 150,
    actions: [
      { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
      { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, disabled: ({ row }) => row.status !== 'active', onClick: ({ row }) => notify(`编辑 ${row.name}`) },
      { key: 'recall', buttonProps: { icon: 'i-lucide-undo-2', label: '召回', size: 'xs' }, visibility: ({ row }) => row.status === 'offboarded', onClick: ({ row }) => notify(`召回 ${row.name}`) }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```

## Shared Fields for Special Columns

`selection`, `index`, `row-pinning` and `actions` columns support the same `visibility`, `pinable`, `resizable`, `size`, `minSize`, `maxSize`, `align` and `fixed` as data columns. Special columns default to a fixed width of 60; providing `minSize` / `maxSize` (without `size`) switches them to auto-sizing — `minSize` is the lower bound and `maxSize` is the drag upper limit. The sole right-side actions column (`fixed: 'right'`) can set `minSize` to expand with the number of buttons. See [Data Column Width Rules](https://nuxt.mhaibaraai.cn/docs/data-table/columns) for details.

```vue [DataTableSpecialSharedExample.vue]
<script setup lang="ts">
import type { DataTableAction, DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const actions: DataTableAction<Person>[] = [
  { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => { toast.add({ title: `查看 ${row.name}`, duration: 1500 }) } }
]

const columns: DataTableColumn<Person>[] = [
  { type: 'selection', pinable: true, resizable: true },
  { type: 'index', visibility: false },
  { type: 'row-pinning', pinable: false },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门', resizable: true },
  { type: 'actions', size: 80, actions }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
```


## Sitemap

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