Table View
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.
<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>-headerand#<column-id>-cellslots to customize columns without render functions. See With Slots.
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | paid | ethan.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.
| ID | Name | Role | ||
|---|---|---|---|---|
| 1 | Lindsay Walton Front-end Developer | lindsay.walton@example.com | Member | |
| 2 | Courtney Henry Designer | courtney.henry@example.com | Admin | |
| 3 | Tom Cook Director of Product | tom.cook@example.com | Member | |
| 4 | Whitney Francis Copywriter | whitney.francis@example.com | Admin | |
| 5 | Leonard Krasner Senior Designer | leonard.krasner@example.com | Owner | |
| 6 | Floyd Miles Principal Designer | floyd.miles@example.com | Member |
<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.
| # | Date | Status | Amount | ||
|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| #4596 | Mar 10, 15:55 | paid | ethan.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
expandedwithv-modelto control the expandable state.
| # | Date | Status | Amount | ||
|---|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
{
"id": "4599",
"date": "2024-03-11T10:10:00",
"status": "failed",
"email": "mia.white@example.com",
"amount": 276
} | |||||
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| #4596 | Mar 10, 15:55 | paid | ethan.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.idby default, sorowSelectionkeys must match theidfield of each data row.
| Date | Status | Amount | ||
|---|---|---|---|---|
| Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 | |
| Mar 11, 10:10 | failed | mia.white@example.com | €276.00 | |
| Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 | |
| Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 | |
| Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 |
<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.
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 |
| #4600 | Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4598 | Mar 11, 08:50 | refunded | william.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.
| #4596 | Mar 10, 15:55 | paid | ethan.harris@example.com | €639.00 |
| #4597 | Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| #4598 | Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| #4599 | Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| #4600 | Mar 11, 15:30 | paid | james.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.
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.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, orshowHeader: falsefor a fully custom filter UI.
| # | Date | Status | Amount | |
|---|---|---|---|---|
| #4600 | Mar 11, 15:30 | paid | james.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.
| Date | Status | Amount | |
|---|---|---|---|
| Mar 11, 15:30 | paid | james.anderson@example.com | €594.00 |
| Mar 11, 10:10 | failed | mia.white@example.com | €276.00 |
| Mar 11, 08:50 | refunded | william.brown@example.com | €315.00 |
| Mar 10, 19:45 | paid | emma.davis@example.com | €529.00 |
| Mar 10, 15:55 | paid | ethan.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.
| #4600000000000000000000000000000000000000 | 2024-03-11T15:30:00 | paid | james.anderson@example.com | €594,000.00 |
| #4599000000000000000000000000000000000000 | 2024-03-11T10:10:00 | failed | mia.white@example.com | €276,000.00 |
| #4598000000000000000000000000000000000000 | 2024-03-11T08:50:00 | refunded | william.brown@example.com | €315,000.00 |
| #4597000000000000000000000000000000000000 | 2024-03-10T19:45:00 | paid | emma.davis@example.com | €5,290,000.00 |
| #4596000000000000000000000000000000000000 | 2024-03-10T15:55:00 | paid | ethan.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.
| # | Date | Amount | |
|---|---|---|---|
| #4600 | 2024-03-11T15:30:00 | james.anderson@example.com | 594 |
| #4599 | 2024-03-11T10:10:00 | mia.white@example.com | 276 |
| #4598 | 2024-03-11T08:50:00 | william.brown@example.com | 315 |
| #4597 | 2024-03-10T19:45:00 | emma.davis@example.com | 529 |
| #4596 | 2024-03-10T15:55:00 | ethan.harris@example.com | 639 |
<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.
| ID | Name | Company | |
|---|---|---|---|
<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ódigo | Nome | Status |
|---|---|---|
No dataNo 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ódigo | Nome | Status |
|---|---|---|
<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
| Prop | Default | Type |
|---|---|---|
columnFilters | ColumnFiltersState Per-column filter state, bound with v-model | |
columnPinning | ColumnPinningState Column pinning state, bound with v-model | |
columns | TableColumn<T>[] Column definitions. See ColumnDef | |
columnVisibility | VisibilityState Column visibility state, bound with v-model | |
data | T[] | () => Promise<T[]> Table rows. Each row must have an id: string field | |
empty | Omit<EmptyProps, 'variant'> & { src?: string } Empty state. See Empty | |
expanded | ExpandedState Row expanded state, bound with v-model | |
getRowId | (row: T) => string Custom row ID (defaults to row.id) | |
globalFilter | string Global filter value, bound with v-model | |
loading | false | Boolean Shows loading indicator |
loadingAnimation | 'carousel' | 'carousel' | 'carousel-inverse' | 'swing' | 'elastic' |
loadingColor | 'primary' | 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral' |
pagination | PaginationState — { pageIndex: number, pageSize: number } Pagination state, bound with v-model | |
rowPinning | RowPinningState Row pinning state, bound with v-model | |
rowSelection | RowSelectionState — Record<string, boolean> Selected rows, bound with v-model | |
searchPlaceholder | String Placeholder for the search input. Falls back to the locale translation when omitted | |
search | true | Boolean Shows or hides the search input in the header bar |
selectable | false | Boolean Adds a checkbox column and shows selection count in footer |
showHeader | true | Boolean Shows or hides the entire header bar |
sorting | SortingState — Array<{ id: string, desc: boolean }> Column sort state, bound with v-model | |
sticky | true | Boolean Sticky header |
ui | Partial<TableUi> Custom classes. See Table theme |
Slots
The block supports all UTable slots via pass-through, plus the additional slots below.
| Slot | Type |
|---|---|
<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 | {} |