Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Guidance for building UIs with Nuxt UI, the official Tailwind-based component library for Nuxt.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/recipes/data-tables.md
1# Data Tables23Complete patterns for displaying and managing tabular data.45## Basic table67```vue8<script setup lang="ts">9import type { TableColumn } from '@nuxt/ui'1011const data = ref([12{ name: 'Alice', email: '[email protected]', role: 'Admin' },13{ name: 'Bob', email: '[email protected]', role: 'Editor' }14])1516const columns: TableColumn<typeof data.value[number]>[] = [{17accessorKey: 'name',18header: 'Name'19}, {20accessorKey: 'email',21header: 'Email'22}, {23accessorKey: 'role',24header: 'Role'25}]26</script>2728<template>29<UTable :data="data" :columns="columns" />30</template>31```3233## With search and filters (dashboard)3435```vue36<script setup lang="ts">37import type { TableColumn } from '@nuxt/ui'3839const search = ref('')40const roleFilter = ref('All')4142const rows = ref([43{ name: 'Alice', email: '[email protected]', role: 'Admin', status: 'Active' },44{ name: 'Bob', email: '[email protected]', role: 'Editor', status: 'Inactive' }45])4647const columns: TableColumn[] = [48{ accessorKey: 'name', header: 'Name' },49{ accessorKey: 'email', header: 'Email' },50{ accessorKey: 'role', header: 'Role' },51{ accessorKey: 'status', header: 'Status' },52{ id: 'actions' }53]5455const filteredRows = computed(() => {56return rows.value.filter(row => {57const matchesSearch = !search.value || row.name.toLowerCase().includes(search.value.toLowerCase())58const matchesRole = roleFilter.value === 'All' || row.role === roleFilter.value59return matchesSearch && matchesRole60})61})62</script>6364<template>65<UDashboardPanel>66<template #header>67<UDashboardNavbar title="Users" />6869<UDashboardToolbar>70<template #left>71<UInput v-model="search" icon="i-lucide-search" placeholder="Search users..." />72</template>73<template #right>74<USelect v-model="roleFilter" :items="['All', 'Admin', 'Editor', 'Viewer']" />75</template>76</UDashboardToolbar>77</template>7879<template #body>80<UTable :data="filteredRows" :columns="columns">81<template #status-cell="{ row }">82<UBadge :color="row.original.status === 'Active' ? 'success' : 'neutral'" :label="row.original.status" variant="subtle" />83</template>8485<template #actions-cell="{ row }">86<UDropdownMenu87:items="[88[{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit(row.original) }],89[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error', onSelect: () => remove(row.original) }]90]"91>92<UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" />93</UDropdownMenu>94</template>95</UTable>96</template>97</UDashboardPanel>98</template>99```100101## With row selection102103Row selection uses TanStack Table's `rowSelection` state — a `Record<string, boolean>` keyed by row index.104105```vue106<script setup lang="ts">107const table = useTemplateRef('table')108const rowSelection = ref<Record<string, boolean>>({})109</script>110111<template>112<UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" />113114<div class="px-4 py-3.5 text-sm text-muted">115{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of116{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.117</div>118</template>119```120121Add a checkbox column using the `h` function. Use tri-state `modelValue` (`true`, `false`, or `'indeterminate'`) for the "select all" header:122123```ts124import { h } from 'vue'125126const UCheckbox = resolveComponent('UCheckbox')127128const columns: TableColumn[] = [{129id: 'select',130header: ({ table }) => h(UCheckbox, {131'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),132'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),133'aria-label': 'Select all'134}),135cell: ({ row }) => h(UCheckbox, {136'modelValue': row.getIsSelected(),137'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),138'aria-label': 'Select row'139})140},141// ... other columns142]143```144145## With pagination146147Use `v-model:pagination` on `UTable` with TanStack's `getPaginationRowModel`, then wire `UPagination` to the table API. `UPagination`'s `total` is total **items** (not pages) — it calculates page count from `total / items-per-page`.148149```vue150<script setup lang="ts">151import { getPaginationRowModel } from '@tanstack/vue-table'152153const table = useTemplateRef('table')154155const pagination = ref({156pageIndex: 0,157pageSize: 5158})159</script>160161<template>162<UTable163ref="table"164v-model:pagination="pagination"165:data="data"166:columns="columns"167:pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"168/>169170<div class="flex justify-end p-4">171<UPagination172:page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"173:items-per-page="table?.tableApi?.getState().pagination.pageSize"174:total="table?.tableApi?.getFilteredRowModel().rows.length"175@update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"176/>177</div>178</template>179```180181## With async data (Nuxt)182183Use `status === 'pending' || status === 'idle'` for loading state — `idle` covers the initial render before `useLazyFetch` starts.184185```vue186<script setup lang="ts">187const { data, status } = useLazyFetch('/api/users', { server: false })188</script>189190<template>191<UTable :data="data" :columns="columns" :loading="status === 'pending' || status === 'idle'" />192</template>193```194195For server-side pagination:196197```vue198<script setup lang="ts">199const page = ref(1)200201const { data, status } = await useAsyncData(202'users',203() => $fetch('/api/users', { query: { page: page.value } }),204{ watch: [page] }205)206</script>207208<template>209<UTable :data="data?.items" :columns="columns" :loading="status === 'pending'" />210211<div class="flex justify-end p-4">212<UPagination v-model="page" :total="data?.total" :items-per-page="data?.pageSize" />213</div>214</template>215```216217## Tips218219- Table is built on [TanStack Table](https://tanstack.com/table/latest) — columns use `ColumnDef` format with `accessorKey`, `header`, `cell`220- Use `#<column>-cell` and `#<column>-header` template slots to customize rendering with Vue templates221- Alternatively, use the `h` function inside `header` and `cell` column properties for inline rendering222- Row data in slots is accessed via `row.original` (not `row` directly)223- Use `v-model:row-selection` for selection, `v-model:sorting` for sort state224- Wrap tables in `UDashboardPanel` with `#header` toolbar for the dashboard pattern225- For empty states, use the `#empty` slot226