Blocks

Table View

A block for visualizing structured data in columns with sorting, pagination and row selection.

Usage

The table renders a header bar by default with a search input bound to globalFilter. Set :search="false" to hide only the search input while keeping the header, or showHeader: false to hide the entire bar.

Loading preview...
<template>
  <MeTableView :data="data" :columns="columns" class="flex-1" />
</template>

<script setup>
const data = ref([
  { id: '4600', date: '2024-03-11', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11', email: 'william.brown@example.com', amount: 315 },
  { id: '4597', date: '2024-03-10', email: 'emma.davis@example.com', amount: 529 },
  { id: '4596', date: '2024-03-10', email: 'ethan.harris@example.com', amount: 639 }
])

const columns = [
  { accessorKey: 'id', header: '#', cell: ({ row }) => `#${row.getValue('id')}` },
  { accessorKey: 'date', header: 'Date' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'amount', header: 'Amount' }
]
</script>

Columns

Use the columns prop as an array of ColumnDef objects. Each column can define an accessorKey, a header, a cell render function and a meta for styling.

TIP: Use the #<column-id>-header and #<column-id>-cell slots to customize columns without render functions. See With Slots.

#DateStatusEmailAmount
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
#4599Mar 11, 10:10failedmia.white@example.com€276.00
#4598Mar 11, 08:50refundedwilliam.brown@example.com€315.00
#4597Mar 10, 19:45paidemma.davis@example.com€529.00
#4596Mar 10, 15:55paidethan.harris@example.com€639.00
<template>
  <MeTableView :data="data" :columns="columns" class="flex-1" />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', status: 'refunded', email: 'william.brown@example.com', amount: 315 },
  { id: '4597', date: '2024-03-10T19:45:00', status: 'paid', email: 'emma.davis@example.com', amount: 529 },
  { id: '4596', date: '2024-03-10T15:55:00', status: 'paid', email: 'ethan.harris@example.com', amount: 639 }
])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => new Date(row.getValue('date')).toLocaleString('en-US', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hour12: false })
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({ paid: 'success', failed: 'error', refunded: 'neutral' } as const)[row.getValue('status') as string]
    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: { class: { th: 'text-right', td: 'text-right font-medium' } },
  cell: ({ row }) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(Number.parseFloat(row.getValue('amount')))
}]
</script>

Custom Slots

Use the #<column-id>-header slot to customize the header of a column and the #<column-id>-cell slot to customize its cells. Both receive the full TanStack row/column scope.

IDNameEmailRole
1
Lindsay Walton avatar

Lindsay Walton

Front-end Developer

lindsay.walton@example.comMember
2
Courtney Henry avatar

Courtney Henry

Designer

courtney.henry@example.comAdmin
3
Tom Cook avatar

Tom Cook

Director of Product

tom.cook@example.comMember
4
Whitney Francis avatar

Whitney Francis

Copywriter

whitney.francis@example.comAdmin
5
Leonard Krasner avatar

Leonard Krasner

Senior Designer

leonard.krasner@example.comOwner
6
Floyd Miles avatar

Floyd Miles

Principal Designer

floyd.miles@example.comMember
<template>
  <MeTableView :data="data" :columns="columns" class="flex-1">
    <template #header>
      <UButton label="Filtros" icon="i-lucide-filter" color="neutral" variant="outline" />
    </template>
    <template #name-cell="{ row }">
      <div class="flex items-center gap-3">
        <UAvatar :src="`https://i.pravatar.cc/120?img=${row.original.id}`" size="lg" loading="lazy" />
        <div>
          <p class="font-medium text-highlighted">{{ row.original.name }}</p>
          <p>{{ row.original.position }}</p>
        </div>
      </div>
    </template>
    <template #action-cell="{ row }">
      <UDropdownMenu :items="getDropdownActions(row.original)">
        <UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" aria-label="Actions" />
      </UDropdownMenu>
    </template>
  </MeTableView>
</template>

<script setup lang="ts">
import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'

interface User {
  id: string
  name: string
  position: string
  email: string
  role: string
}

const data = ref<User[]>([
  { id: '1', name: 'Lindsay Walton', position: 'Front-end Developer', email: 'lindsay.walton@example.com', role: 'Member' },
  { id: '2', name: 'Courtney Henry', position: 'Designer', email: 'courtney.henry@example.com', role: 'Admin' },
  { id: '3', name: 'Tom Cook', position: 'Director of Product', email: 'tom.cook@example.com', role: 'Member' }
])

const columns: TableColumn<User>[] = [
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'role', header: 'Role' },
  { id: 'action' }
]

function getDropdownActions(user: User): DropdownMenuItem[][] {
  return [
    [{ label: 'Edit', icon: 'i-lucide-edit' }],
    [{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }]
  ]
}
</script>

Row Actions

Add an actions column that renders a DropdownMenu inside the cell or via the #actions-cell slot for per-row operations.

#DateStatusEmailAmount
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
#4599Mar 11, 10:10failedmia.white@example.com€276.00
#4598Mar 11, 08:50refundedwilliam.brown@example.com€315.00
#4597Mar 10, 19:45paidemma.davis@example.com€529.00
#4596Mar 10, 15:55paidethan.harris@example.com€639.00
<template>
  <MeTableView :data="data" :columns="columns" class="flex-1" />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Row } from '@tanstack/vue-table'

const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')

type Payment = { id: string; date: string; status: 'paid' | 'failed' | 'refunded'; email: string; amount: number }

const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', status: 'refunded', email: 'william.brown@example.com', amount: 315 }
])

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'id', header: '#', cell: ({ row }) => `#${row.getValue('id')}` },
  { accessorKey: 'status', header: 'Status', cell: ({ row }) => h(UBadge, { class: 'capitalize', variant: 'subtle', color: ({ paid: 'success', failed: 'error', refunded: 'neutral' } as const)[row.getValue('status') as string] }, () => row.getValue('status')) },
  { accessorKey: 'email', header: 'Email' },
  {
    id: 'actions',
    meta: { class: { td: 'text-right' } },
    cell: ({ row }) => h(UDropdownMenu, { content: { align: 'end' }, items: getRowItems(row) }, () => h(UButton, { icon: 'i-lucide-ellipsis-vertical', color: 'neutral', variant: 'ghost' }))
  }
]

function getRowItems(row: Row<Payment>) {
  return [
    { type: 'label', label: 'Actions' },
    { label: 'Copy payment ID', onSelect() { console.log('copy', row.original.id) } },
    { type: 'separator' },
    { label: 'View customer' },
    { label: 'View payment details' }
  ]
}
</script>

Expandable Rows

Add an expand column that renders a Button to toggle row expansion. Define the #expanded slot to render the expanded content, which receives the row as a parameter.

TIP: Bind expanded with v-model to control the expandable state.

#DateStatusEmailAmount
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
#4599Mar 11, 10:10failedmia.white@example.com€276.00
{
  "id": "4599",
  "date": "2024-03-11T10:10:00",
  "status": "failed",
  "email": "mia.white@example.com",
  "amount": 276
}
#4598Mar 11, 08:50refundedwilliam.brown@example.com€315.00
#4597Mar 10, 19:45paidemma.davis@example.com€529.00
#4596Mar 10, 15:55paidethan.harris@example.com€639.00
<template>
  <MeTableView
    v-model:expanded="expanded"
    :data="data"
    :columns="columns"
    :ui="{ tr: 'data-[expanded=true]:bg-elevated/50' }"
    class="flex-1"
  >
    <template #expanded="{ row }">
      <pre>{{ row.original }}</pre>
    </template>
  </MeTableView>
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { ExpandedState } from '@tanstack/vue-table'

const UButton = resolveComponent('UButton')

type Payment = { id: string; date: string; status: string; email: string; amount: number }

const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', status: 'refunded', email: 'william.brown@example.com', amount: 315 }
])

const columns: TableColumn<Payment>[] = [{
  id: 'expand',
  cell: ({ row }) => h(UButton, {
    color: 'neutral', variant: 'ghost', icon: 'i-lucide-chevron-down', square: true,
    ui: { leadingIcon: ['transition-transform', row.getIsExpanded() ? 'duration-200 rotate-180' : ''] },
    onClick: () => row.toggleExpanded()
  })
}, {
  accessorKey: 'id', header: '#', cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'email', header: 'Email'
}, {
  accessorKey: 'amount', header: 'Amount'
}]

const expanded = ref<ExpandedState>({ '4599': true })
</script>

Row Selection

Use the selectable prop to add a checkbox column automatically. Bind rowSelection with v-model to track selected rows. The footer shows a selection counter.

TIP: Row IDs are resolved from row.id by default, so rowSelection keys must match the id field of each data row.

DateStatusEmailAmount
Mar 11, 15:30paidjames.anderson@example.com€594.00
Mar 11, 10:10failedmia.white@example.com€276.00
Mar 11, 08:50refundedwilliam.brown@example.com€315.00
Mar 10, 19:45paidemma.davis@example.com€529.00
Mar 10, 15:55paidethan.harris@example.com€639.00
1 of 5 row(s) selected.
<template>
  <MeTableView
    v-model:row-selection="rowSelection"
    :data="data"
    :columns="columns"
    selectable
  />
</template>

<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import type { RowSelectionState } from '@tanstack/vue-table'

type Payment = { id: string; date: string; status: string; email: string; amount: number }

const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', status: 'refunded', email: 'william.brown@example.com', amount: 315 }
])

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'date', header: 'Date' },
  { accessorKey: 'status', header: 'Status' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'amount', header: 'Amount' }
]

const rowSelection = ref<RowSelectionState>({ '4599': true })
</script>

Column Sorting

Update a column header to render a Button that calls column.toggleSorting(). Bind sorting with v-model to control the sort state.

TIP: You can also create a reusable sort header using a DropdownMenu. See the next example.

#DateStatusAmount
#4597Mar 10, 19:45paidemma.davis@example.com€529.00
#4596Mar 10, 15:55paidethan.harris@example.com€639.00
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
#4599Mar 11, 10:10failedmia.white@example.com€276.00
#4598Mar 11, 08:50refundedwilliam.brown@example.com€315.00
<template>
  <MeTableView v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { SortingState } from '@tanstack/vue-table'

const UButton = resolveComponent('UButton')

type Payment = { id: string; date: string; status: string; email: string; amount: number }

const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 }
])

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'email',
  header: ({ column }) => {
    const isSorted = column.getIsSorted()
    return h(UButton, {
      color: 'neutral', variant: 'ghost', label: 'Email',
      icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
      class: '-mx-2.5',
      onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
    })
  }
}, {
  accessorKey: 'amount', header: 'Amount'
}]

const sorting = ref<SortingState>([{ id: 'email', desc: false }])
</script>

You can also create a reusable getHeader function that wraps a DropdownMenu to select sort direction.

#4596Mar 10, 15:55paidethan.harris@example.com€639.00
#4597Mar 10, 19:45paidemma.davis@example.com€529.00
#4598Mar 11, 08:50refundedwilliam.brown@example.com€315.00
#4599Mar 11, 10:10failedmia.white@example.com€276.00
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
<template>
  <MeTableView v-model:sorting="sorting" :data="data" :columns="columns" class="flex-1" />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column, SortingState } from '@tanstack/vue-table'

const UButton = resolveComponent('UButton')
const UDropdownMenu = resolveComponent('UDropdownMenu')

type Payment = { id: string; email: string; amount: number }
const data = ref<Payment[]>([
  { id: '4600', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', email: 'mia.white@example.com', amount: 276 }
])

function getHeader(column: Column<Payment>, label: string) {
  const isSorted = column.getIsSorted()
  return h(UDropdownMenu, {
    content: { align: 'start' },
    items: [{
      label: 'Asc', type: 'checkbox', icon: 'i-lucide-arrow-up-narrow-wide', checked: isSorted === 'asc',
      onSelect: () => column.getIsSorted() === 'asc' ? column.clearSorting() : column.toggleSorting(false)
    }, {
      label: 'Desc', type: 'checkbox', icon: 'i-lucide-arrow-down-wide-narrow', checked: isSorted === 'desc',
      onSelect: () => column.getIsSorted() === 'desc' ? column.clearSorting() : column.toggleSorting(true)
    }]
  }, () => h(UButton, {
    color: 'neutral', variant: 'ghost', label,
    icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down',
    class: '-mx-2.5 data-[state=open]:bg-elevated'
  }))
}

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'id', header: ({ column }) => getHeader(column, 'ID'), cell: ({ row }) => `#${row.getValue('id')}` },
  { accessorKey: 'email', header: ({ column }) => getHeader(column, 'Email') },
  { accessorKey: 'amount', header: ({ column }) => getHeader(column, 'Amount') }
]

const sorting = ref<SortingState>([{ id: 'id', desc: false }])
</script>

Global Filter

Use a Input component to filter rows across all fields. Bind globalFilter with v-model.

#DateStatusEmailAmount
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
<template>
  <MeTableView
    v-model:global-filter="globalFilter"
    :data="data"
    :columns="columns"
    class="flex-1"
  />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'

const UBadge = resolveComponent('UBadge')

type Payment = {
  id: string
  date: string
  status: 'paid' | 'failed' | 'refunded'
  email: string
  amount: number
}

const data: Payment[] = [{
  id: '4600',
  date: '2024-03-11T15:30:00',
  status: 'paid',
  email: 'james.anderson@example.com',
  amount: 594
}, {
  id: '4599',
  date: '2024-03-11T10:10:00',
  status: 'failed',
  email: 'mia.white@example.com',
  amount: 276
}, {
  id: '4598',
  date: '2024-03-11T08:50:00',
  status: 'refunded',
  email: 'william.brown@example.com',
  amount: 315
}, {
  id: '4597',
  date: '2024-03-10T19:45:00',
  status: 'paid',
  email: 'emma.davis@example.com',
  amount: 529
}, {
  id: '4596',
  date: '2024-03-10T15:55:00',
  status: 'paid',
  email: 'ethan.harris@example.com',
  amount: 639
}]

const columns: TableColumn<Payment>[] = [{
  accessorKey: 'id',
  header: '#',
  cell: ({ row }) => `#${row.getValue('id')}`
}, {
  accessorKey: 'date',
  header: 'Date',
  cell: ({ row }) => {
    return new Date(row.getValue('date')).toLocaleString('en-US', {
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false
    })
  }
}, {
  accessorKey: 'status',
  header: 'Status',
  cell: ({ row }) => {
    const color = ({
      paid: 'success' as const,
      failed: 'error' as const,
      refunded: 'neutral' as const
    })[row.getValue('status') as string]

    return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
  }
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'amount',
  header: 'Amount',
  meta: {
    class: {
      th: 'text-right',
      td: 'text-right font-medium'
    }
  },
  cell: ({ row }) => {
    const amount = Number.parseFloat(row.getValue('amount'))
    return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(amount)
  }
}]

const globalFilter = ref('james')
</script>

Column Filters

Use a Input component to filter per-column. Bind columnFilters with v-model and use a computed to read/write a specific column's filter value.

TIP: Column filters target specific columns and require an external input — they are not surfaced by the built-in header. Use :search="false" to hide the built-in search input, or showHeader: false for a fully custom filter UI.

#DateStatusEmailAmount
#4600Mar 11, 15:30paidjames.anderson@example.com€594.00
<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex px-4 py-3.5 border-b border-accented">
      <UInput v-model="emailFilter" class="max-w-sm" placeholder="Filter emails..." />
    </div>
    <MeTableView v-model:column-filters="columnFilters" :data="data" :columns="columns" />
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { ColumnFiltersState } from '@tanstack/vue-table'

type Payment = { id: string; date: string; status: string; email: string; amount: number }
const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', status: 'refunded', email: 'william.brown@example.com', amount: 315 }
])

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'id', header: '#' },
  { accessorKey: 'status', header: 'Status' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'amount', header: 'Amount' }
]

const columnFilters = ref<ColumnFiltersState>([{ id: 'email', value: 'james' }])

const emailFilter = computed({
  get: () => (columnFilters.value.find(f => f.id === 'email')?.value as string) ?? '',
  set: (v: string) => {
    const others = columnFilters.value.filter(f => f.id !== 'email')
    columnFilters.value = v ? [...others, { id: 'email', value: v }] : others
  }
})
</script>

Column Visibility

Use a DropdownMenu to toggle column visibility. Bind columnVisibility with v-model and build the items from the columns array.

DateStatusEmailAmount
Mar 11, 15:30paidjames.anderson@example.com€594.00
Mar 11, 10:10failedmia.white@example.com€276.00
Mar 11, 08:50refundedwilliam.brown@example.com€315.00
Mar 10, 19:45paidemma.davis@example.com€529.00
Mar 10, 15:55paidethan.harris@example.com€639.00
<template>
  <div class="flex flex-col flex-1 w-full">
    <div class="flex justify-end px-4 py-3.5 border-b border-accented">
      <UDropdownMenu :items="columnVisibilityItems" :content="{ align: 'end' }">
        <UButton label="Columns" color="neutral" variant="outline" trailing-icon="i-lucide-chevron-down" />
      </UDropdownMenu>
    </div>
    <MeTableView v-model:column-visibility="columnVisibility" :data="data" :columns="columns" />
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'
import type { VisibilityState } from '@tanstack/vue-table'

type Payment = { id: string; date: string; status: string; email: string; amount: number }
const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', status: 'paid', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', status: 'failed', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', status: 'refunded', email: 'william.brown@example.com', amount: 315 }
])

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'id', header: '#' },
  { accessorKey: 'date', header: 'Date' },
  { accessorKey: 'status', header: 'Status' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'amount', header: 'Amount' }
]

const columnVisibility = ref<VisibilityState>({ id: false })

const columnVisibilityItems = computed(() =>
  columns.filter(col => col.enableHiding !== false).map((col) => {
    const key = (col.id ?? col.accessorKey) as string
    return {
      label: upperFirst(key),
      type: 'checkbox' as const,
      checked: columnVisibility.value[key] !== false,
      onUpdateChecked(checked: boolean) { columnVisibility.value = { ...columnVisibility.value, [key]: checked } },
      onSelect(e: Event) { e.preventDefault() }
    }
  })
)
</script>

Column Pinning

Update a column header to render a Button that calls column.pin(). Bind columnPinning with v-model. Set explicit size values on columns for correct width handling when pinned.

#46000000000000000000000000000000000000002024-03-11T15:30:00paidjames.anderson@example.com€594,000.00
#45990000000000000000000000000000000000002024-03-11T10:10:00failedmia.white@example.com€276,000.00
#45980000000000000000000000000000000000002024-03-11T08:50:00refundedwilliam.brown@example.com€315,000.00
#45970000000000000000000000000000000000002024-03-10T19:45:00paidemma.davis@example.com€5,290,000.00
#45960000000000000000000000000000000000002024-03-10T15:55:00paidethan.harris@example.com€639,000.00
<template>
  <MeTableView v-model:column-pinning="columnPinning" :data="data" :columns="columns" class="flex-1" />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
import type { Column, ColumnPinningState } from '@tanstack/vue-table'

const UButton = resolveComponent('UButton')

type Payment = { id: string; date: string; status: string; email: string; amount: number }
const data = ref<Payment[]>([
  { id: '4600000000000000000000', date: '2024-03-11', status: 'paid', email: 'james.anderson@example.com', amount: 594000 },
  { id: '4599000000000000000000', date: '2024-03-11', status: 'failed', email: 'mia.white@example.com', amount: 276000 }
])

function getHeader(column: Column<Payment>, label: string, position: 'left' | 'right') {
  const isPinned = column.getIsPinned()
  return h(UButton, {
    color: 'neutral', variant: 'ghost', label,
    icon: isPinned ? 'i-lucide-pin-off' : 'i-lucide-pin',
    class: '-mx-2.5',
    onClick() { column.pin(isPinned === position ? false : position) }
  })
}

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'id', header: ({ column }) => getHeader(column, 'ID', 'left'), size: 220 },
  { accessorKey: 'date', header: ({ column }) => getHeader(column, 'Date', 'left'), size: 172 },
  { accessorKey: 'status', header: ({ column }) => getHeader(column, 'Status', 'left'), size: 103 },
  { accessorKey: 'email', header: ({ column }) => getHeader(column, 'Email', 'left'), size: 232 },
  { accessorKey: 'amount', header: ({ column }) => getHeader(column, 'Amount', 'right'), size: 130 }
]

const columnPinning = ref<ColumnPinningState>({ left: ['id'], right: ['amount'] })
</script>

Pagination

Bind pagination with v-model to control page index and page size. Client-side pagination is activated automatically — getPaginationRowModel is wired in by the block when a pagination binding is present.

#DateEmailAmount
#46002024-03-11T15:30:00james.anderson@example.com594
#45992024-03-11T10:10:00mia.white@example.com276
#45982024-03-11T08:50:00william.brown@example.com315
#45972024-03-10T19:45:00emma.davis@example.com529
#45962024-03-10T15:55:00ethan.harris@example.com639
<template>
  <MeTableView
    v-model:pagination="pagination"
    v-model:global-filter="globalFilter"
    :data="data"
    :columns="columns"
  />
</template>

<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import type { PaginationState } from '@tanstack/vue-table'

type Payment = { id: string; date: string; email: string; amount: number }
const data = ref<Payment[]>([
  { id: '4600', date: '2024-03-11T15:30:00', email: 'james.anderson@example.com', amount: 594 },
  { id: '4599', date: '2024-03-11T10:10:00', email: 'mia.white@example.com', amount: 276 },
  { id: '4598', date: '2024-03-11T08:50:00', email: 'william.brown@example.com', amount: 315 },
  { id: '4597', date: '2024-03-10T19:45:00', email: 'emma.davis@example.com', amount: 529 },
  { id: '4596', date: '2024-03-10T15:55:00', email: 'ethan.harris@example.com', amount: 639 },
  { id: '4595', date: '2024-03-10T13:20:00', email: 'sophia.miller@example.com', amount: 428 },
  { id: '4594', date: '2024-03-10T11:05:00', email: 'noah.wilson@example.com', amount: 673 },
  { id: '4593', date: '2024-03-09T22:15:00', email: 'olivia.jones@example.com', amount: 382 }
])

const columns: TableColumn<Payment>[] = [
  { accessorKey: 'id', header: '#', cell: ({ row }) => `#${row.getValue('id')}` },
  { accessorKey: 'date', header: 'Date' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'amount', header: 'Amount' }
]

const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const globalFilter = ref('')
</script>

Fetched Data

Pass an async function to the data prop to fetch rows from an API. A loading indicator is shown automatically while the function resolves.

IDNameEmailCompany
<template>
  <MeTableView :data="provider" :columns="columns" class="flex-1 h-80" />
</template>

<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import type { AvatarProps, TableColumn } from '@nuxt/ui'

const UAvatar = resolveComponent('UAvatar')

type User = {
  id: string
  name: string
  username: string
  email: string
  avatar: AvatarProps
  company: { name: string }
}

async function provider(): Promise<User[]> {
  const data = await $fetch<Array<{ id: number; name: string; username: string; email: string; company: { name: string } }>>(
    'https://jsonplaceholder.typicode.com/users'
  )
  return data.map(user => ({
    ...user,
    id: String(user.id),
    avatar: { src: `https://i.pravatar.cc/120?img=${user.id}`, alt: `${user.name} avatar` }
  }))
}

const columns: TableColumn<User>[] = [{
  accessorKey: 'id', header: 'ID'
}, {
  accessorKey: 'name',
  header: 'Name',
  cell: ({ row }) => h('div', { class: 'flex items-center gap-3' }, [
    h(UAvatar, { ...row.original.avatar, loading: 'lazy', size: 'lg' }),
    h('div', undefined, [
      h('p', { class: 'font-medium text-highlighted' }, row.original.name),
      h('p', {}, `@${row.original.username}`)
    ])
  ])
}, {
  accessorKey: 'email', header: 'Email'
}, {
  accessorKey: 'company', header: 'Company', cell: ({ row }) => row.original.company.name
}]
</script>

Empty

Use the empty prop to customize the empty state when no data is provided. It accepts all props from Empty (except variant), plus a src field to customize the image.

const empty: Omit<EmptyProps, 'variant'> & { src?: string }
CódigoNomeStatus

No data

No data available
<template>
  <MeTableView
    :columns="columns"
    :empty="{
      title: 'No data',
      description: 'No data available',
      actions: [{ label: 'Action' }]
    }"
  />
</template>

<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'

type Row = { id: string; code: string; name: string; status: string }

const columns: TableColumn<Row>[] = [
  { accessorKey: 'code', header: 'Código' },
  { accessorKey: 'name', header: 'Nome' },
  { accessorKey: 'status', header: 'Status' }
]
</script>

Loading

Use the loading prop to display a loading state. Customize with loading-color and loading-animation.

CódigoNomeStatus
<template>
  <MeTableView :columns="columns" loading />
</template>

<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'

type Row = { id: string; code: string; name: string; status: string }

const columns: TableColumn<Row>[] = [
  { accessorKey: 'code', header: 'Código' },
  { accessorKey: 'name', header: 'Nome' },
  { accessorKey: 'status', header: 'Status' }
]
</script>

API

Props

PropDefaultType
columnFiltersColumnFiltersState
Per-column filter state, bound with v-model
columnPinningColumnPinningState
Column pinning state, bound with v-model
columnsTableColumn<T>[]
Column definitions. See ColumnDef
columnVisibilityVisibilityState
Column visibility state, bound with v-model
dataT[] | () => Promise<T[]>
Table rows. Each row must have an id: string field
emptyOmit<EmptyProps, 'variant'> & { src?: string }
Empty state. See Empty
expandedExpandedState
Row expanded state, bound with v-model
getRowId(row: T) => string
Custom row ID (defaults to row.id)
globalFilterstring
Global filter value, bound with v-model
loadingfalseBoolean
Shows loading indicator
loadingAnimation'carousel''carousel' | 'carousel-inverse' | 'swing' | 'elastic'
loadingColor'primary''primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
paginationPaginationState{ pageIndex: number, pageSize: number }
Pagination state, bound with v-model
rowPinningRowPinningState
Row pinning state, bound with v-model
rowSelectionRowSelectionStateRecord<string, boolean>
Selected rows, bound with v-model
searchPlaceholderString
Placeholder for the search input. Falls back to the locale translation when omitted
searchtrueBoolean
Shows or hides the search input in the header bar
selectablefalseBoolean
Adds a checkbox column and shows selection count in footer
showHeadertrueBoolean
Shows or hides the entire header bar
sortingSortingStateArray<{ id: string, desc: boolean }>
Column sort state, bound with v-model
stickytrueBoolean
Sticky header
uiPartial<TableUi>
Custom classes. See Table theme

Slots

The block supports all UTable slots via pass-through, plus the additional slots below.

SlotType
<column-id>-header{ column, header, table }
<column-id>-cell{ cell, column, getValue, renderValue, row, table }
expanded{ row: TableRow<T> }
empty{}
header{}
footer{}
loading{}
caption{}
body-top{}
body-bottom{}