Examples
Basic
Prop | Default | Type | Description |
---|---|---|---|
columns | [] | Array | Table columns. |
data | [] | Array | Table data. |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Lauretta | Franecki | 36 | 645 | relationship | 81 |
Bertha | Smitham | 6 | 533 | relationship | 60 |
Margaret | Jast | 20 | 329 | single | 33 |
Alexandre | Sanford | 35 | 227 | relationship | 72 |
Damon | O'Connell | 16 | 840 | single | 78 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
</script>
<template>
<NTable
:columns
:data
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Row Selection
Row selection allows you to select rows in the table. This is useful when you want to select rows in the table.
Prop | Default | Type | Description |
---|---|---|---|
modelValue | [] | Array | Selected rows. |
enableRowSelection | false | Boolean | Enable row selection. |
enableMultiRowSelection | false | Boolean | Enable multiple row selection. |
rowId | id | String | Row id to uniquely identify each row. |
enableSubRowSelection | false | Boolean | Enable sub row selection. |
@select | - | Event | Emitted when a row is selected. |
@select-all | - | Event | Emitted when all rows are selected. |
First Name | Last Name | Age | Visits | Status | Profile Progress | |
---|---|---|---|---|---|---|
Jed | Hansen | 23 | 211 | single | 67 | |
Jasmin | Lebsack | 26 | 834 | single | 61 | |
Zachariah | Barrows | 29 | 401 | single | 56 | |
Kenneth | Mitchell | 20 | 373 | relationship | 10 | |
Barton | Kiehn | 26 | 189 | relationship | 5 | |
Delfina | Wilkinson | 36 | 599 | relationship | 93 | |
Marlon | Bahringer | 21 | 332 | relationship | 38 | |
Ashtyn | Emard | 28 | 891 | relationship | 0 | |
Myrl | Schoen | 0 | 224 | single | 40 | |
Hellen | Moen-Goldner | 22 | 520 | complicated | 50 | |
<script setup lang="ts">
import type { ColumnDef, Table } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(10))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const select = ref()
const table = useTemplateRef<Table<Person>>('table')
</script>
<template>
<div class="flex flex-col space-y-4">
<NTable
ref="table"
v-model="select"
:columns
:data
enable-row-selection
/>
<div
class="flex items-center justify-between px-2"
>
<div
class="flex-1 text-sm text-muted"
>
{{ table?.getFilteredSelectedRowModel().rows.length }} of
{{ table?.getFilteredRowModel().rows.length }} row(s) selected.
</div>
</div>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Loading
Loading allows you to show a loading progress indicator in the table. This is useful when you want to show a loading progress indicator in the table.
Prop | Default | Type | Description |
---|---|---|---|
loading | false | Boolean | Loading state. |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Colten | Moore | 24 | 147 | relationship | 24 |
Dina | Rowe | 34 | 162 | relationship | 23 |
Orion | Koepp-Gleason | 0 | 814 | single | 94 |
Cloyd | Runolfsdottir | 25 | 538 | complicated | 46 |
Shaylee | Breitenberg | 19 | 341 | complicated | 92 |
Trent | Ebert | 38 | 866 | single | 57 |
Camren | Monahan | 24 | 7 | relationship | 52 |
Myra | Breitenberg | 23 | 666 | complicated | 22 |
Loraine | Huels | 27 | 191 | complicated | 90 |
Loma | Cole | 9 | 323 | single | 65 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(50))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const loading = ref(true)
</script>
<template>
<div class="flex flex-col space-y-2">
<NCheckbox
v-model:checked="loading"
label="Loading"
/>
<NTable
:loading
:columns
:data
/>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Pagination
Pagination allows you to paginate rows in the table. This is useful when you want to paginate rows in the table.
Prop | Default | Type | Description |
---|---|---|---|
pagination | {pageIndex: 0, pageSize: 10} | {pageIndex: Number, pageSize: Number} | Pagination default configuration. |
manualPagination | false | Boolean | Enable manual pagination. |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Cayla | Mayert | 36 | 778 | single | 76 |
Kody | Gerlach | 28 | 39 | single | 77 |
Diego | Gerhold | 21 | 18 | relationship | 91 |
Adolfo | Ruecker | 15 | 871 | single | 99 |
Ellen | Durgan | 24 | 293 | single | 30 |
<script setup lang="ts">
import type { ColumnDef, Table } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(50))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const pagination = ref({
pageSize: 5,
pageIndex: 0,
})
const table = useTemplateRef<Table<Person>>('table')
</script>
<template>
<div class="flex flex-col space-y-4">
<!-- table -->
<NTable
ref="table"
:pagination="pagination"
:columns
:data
/>
<!-- pagination -->
<div
class="flex flex-wrap items-center justify-between gap-4 overflow-auto px-2"
>
<div
class="flex items-center justify-center text-sm font-medium"
>
Page {{ (table?.getState().pagination.pageIndex ?? 0) + 1 }} of
{{ table?.getPageCount().toLocaleString() }}
</div>
<NPagination
:page="(table?.getState().pagination.pageIndex ?? 0) + 1"
:total="table?.getFilteredRowModel().rows.length"
show-edges
:items-per-page="table?.getState().pagination.pageSize"
@update:page="table?.setPageIndex($event - 1)"
/>
</div>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Sorting
Sorting allows you to sort columns in ascending or descending order. This is useful when you want to sort columns in the table.
Prop | Default | Type | Description |
---|---|---|---|
enableSorting | - | boolean | Enable all column sorting |
column.enableSorting | - | boolean | Enable specific column sorting |
enableMultiColumnSort | - | boolean | Enable multi-column sorting |
Status | |||||
---|---|---|---|---|---|
Aimee | Goyette | 9 | 328 | relationship | 63 |
Rashad | Watsica | 31 | 986 | single | 82 |
Kellie | Green | 40 | 869 | single | 39 |
Mike | Prohaska | 37 | 689 | complicated | 21 |
Bertram | Wisoky | 2 | 190 | single | 81 |
Mafalda | Russel | 0 | 5 | complicated | 97 |
Sandrine | Mraz | 2 | 157 | single | 64 |
Elvie | Mills | 38 | 290 | single | 40 |
Mikayla | Mills | 13 | 427 | complicated | 89 |
Eileen | Metz | 19 | 840 | single | 6 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(50))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
enableSorting: false,
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
</script>
<template>
<NTable
:columns
:data
enable-sorting
enable-multi-sort
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Visibility
Visibility allows you to show or hide columns in the table. This is useful when you want to show or hide columns in the table.
Prop | Default | Type | Description |
---|---|---|---|
columnVisibility | - | object | The column visibility state |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Rhea | Sanford | 1 | 465 | relationship | 71 |
Ozella | Skiles | 37 | 989 | relationship | 35 |
Arielle | Howell | 30 | 481 | single | 45 |
Rico | Lubowitz | 32 | 98 | relationship | 40 |
Keith | McCullough | 3 | 507 | complicated | 6 |
<script setup lang="ts">
import type { ColumnDef, Table } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const table = useTemplateRef<Table<Person>>('table')
const columnVisibility = ref({})
</script>
<template>
<div>
<div class="flex flex-wrap gap-4">
<NCheckbox
v-for="tableColumn in table?.getAllLeafColumns()"
:key="tableColumn.id"
:checked="tableColumn.getIsVisible()"
:label="tableColumn.id"
@update:checked="tableColumn.toggleVisibility()"
/>
</div>
<NSeparator />
<NTable
ref="table"
:column-visibility="columnVisibility"
:columns
:data
/>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Global Filtering
Global filtering allows you to filter rows based on the value entered in the filter input. This is useful when you want to filter rows in the table.
Prop | Default | Type | Description |
---|---|---|---|
globalFilter | - | string | The global filter value |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Coby | Turner | 6 | 515 | relationship | 36 |
Pearl | Wolff | 12 | 919 | complicated | 30 |
Bruce | Crist | 0 | 324 | single | 4 |
Alyson | Reichel | 15 | 755 | relationship | 71 |
Domenick | Oberbrunner | 30 | 513 | complicated | 23 |
Virginie | Prosacco | 31 | 623 | single | 84 |
Francesco | Larkin | 14 | 483 | single | 74 |
Damaris | Willms-Parisian | 3 | 649 | relationship | 79 |
Vicenta | Luettgen | 18 | 464 | complicated | 59 |
Hipolito | Stark | 16 | 679 | complicated | 68 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(10))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const search = ref('')
</script>
<template>
<div class="flex flex-col space-y-4">
<div class="flex flex-wrap items-center justify-between gap-4">
<NInput
v-model="search"
placeholder="Search"
:una="{
inputWrapper: 'w-full md:w-80',
}"
/>
<NButton
label="Add new"
disabled
leading="i-radix-icons-plus"
class="w-full md:w-auto"
/>
</div>
<NTable
:columns
:global-filter="search"
:data
/>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Column Filtering
Column filtering allows you to filter columns based on the value entered in the filter input. This is useful when you want to filter columns in the table.
Prop | Default | Type | Description |
---|---|---|---|
enableColumnFilter | - | boolean | Enable all column filtering |
column.enableColumnFilter | - | boolean | Enable specific column filtering |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Idell | Considine-McLaughlin | 1 | 182 | complicated | 96 |
Sabryna | Larson | 30 | 517 | single | 28 |
Beryl | Funk | 39 | 861 | complicated | 99 |
Josie | Wyman | 14 | 869 | single | 32 |
Ariel | Schimmel | 19 | 86 | complicated | 92 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
enableColumnFilter: false,
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
</script>
<template>
<NTable
:columns
enable-column-filters
:data
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Column Ordering
Column ordering allows you to reorder columns by dragging and dropping them. This is useful when you want to change the order of columns in the table.
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Lue | Braun | 3 | 778 | relationship | 20 |
Blanche | Lynch-Jakubowski | 28 | 37 | complicated | 46 |
Dayna | Harris | 5 | 552 | complicated | 16 |
Grace | Lakin | 38 | 445 | complicated | 38 |
Adelbert | Pfeffer | 37 | 56 | complicated | 85 |
<script setup lang="ts">
import type { ColumnDef, Table } from '@tanstack/vue-table'
import type { Person } from './makeData'
import { faker } from '@faker-js/faker'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: () => 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const table = useTemplateRef<Table<Person>>('table')
function randomizeColumns() {
table.value?.setColumnOrder(faker.helpers.shuffle(table.value?.getAllLeafColumns().map(d => d.id)))
}
</script>
<template>
<div class="flex justify-end">
<NButton
label="Randomize columns"
class="mb-4"
@click="randomizeColumns"
/>
</div>
<!-- table -->
<NTable
ref="table"
:columns
:data
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Column Pinning
Column pinning allows you to pin columns to the left
or right
of the table. This is useful when you have a large number of columns and you want to keep some columns in view while scrolling.
Prop | Default | Type | Description |
---|---|---|---|
columnPins | - | { left: Array, right: Array } | Defines which columns are pinned to the left or right. |
Status | First Name | Last Name | Age | Visits | Profile Progress |
---|---|---|---|---|---|
complicated | Juliet | Frami | 9 | 378 | 85 |
single | Filomena | Kuhn | 16 | 520 | 86 |
single | Gene | O'Conner | 15 | 850 | 12 |
complicated | Erin | Marquardt | 19 | 490 | 16 |
relationship | Lia | Dickinson | 7 | 715 | 64 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: () => 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
</script>
<template>
<NTable
:columns
:data
:column-pinning="{
left: ['status'],
right: ['priority'],
}"
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Expanding
Expanding allows you to expand rows to show additional information. This is useful when you want to show additional information about a row.
First Name | Last Name | Age | Visits | Status | Profile Progress | |
---|---|---|---|---|---|---|
Rodolfo | Lesch | 31 | 142 | relationship | 89 | |
Chris | D'Amore | 18 | 125 | relationship | 99 | |
Percival | Schaden | 7 | 223 | relationship | 11 | |
Alford | Daniel | 26 | 940 | single | 4 | |
Euna | Nicolas | 15 | 826 | single | 66 | |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
const expanded = ref<Record<string, boolean>>({})
</script>
<template>
<NTable
v-model:expanded="expanded"
:columns
:data
>
<template #expanded="{ row }">
<div class="p-4">
<p class="text-sm text-muted">
Object:
</p>
<p class="text-base">
{{ row }}
</p>
</div>
</template>
</NTable>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Grouping
Grouping allows you to group rows based on a column value. This is useful when you want to group rows in the table.
Name | Info | ||||
---|---|---|---|---|---|
First Name | Last Name | Age | Visits | Status | Profile Progress |
Willa | Durgan | 10 | 667 | single | 95 |
Felicity | Kunde | 6 | 542 | complicated | 75 |
Orion | Gerlach | 2 | 401 | relationship | 49 |
Amalia | Armstrong | 26 | 368 | relationship | 98 |
Alyce | Corwin | 31 | 656 | complicated | 87 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import { makeData } from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'Name',
enableSorting: false,
columns: [
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
],
},
{
header: 'Info',
columns: [
{
header: () => 'Age',
accessorKey: 'age',
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
],
},
]
const sorting = ref()
</script>
<template>
<NTable
v-model:sorting="sorting"
:columns
:data
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Server-side
Allows you to fetch data from the server. This is useful when you want to fetch data from the server.
Name | Url |
---|---|
bulbasaur | https://pokeapi.co/api/v2/pokemon/1/ |
ivysaur | https://pokeapi.co/api/v2/pokemon/2/ |
venusaur | https://pokeapi.co/api/v2/pokemon/3/ |
charmander | https://pokeapi.co/api/v2/pokemon/4/ |
charmeleon | https://pokeapi.co/api/v2/pokemon/5/ |
<script setup lang="ts">
import type { ColumnDef, Table } from '@tanstack/vue-table'
interface Pokemon {
name: string
url: string
}
interface ResourceMeta {
count: number
next: string | null
previous: string | null
results: Pokemon[]
}
const pagination = ref({
pageSize: 5,
pageIndex: 0,
})
const endpoint = computed (() => {
const { pageSize, pageIndex } = pagination.value
return `https://pokeapi.co/api/v2/pokemon?limit=${pageSize}&offset=${pageSize * pageIndex}`
})
const { data: resource, refresh, status } = await useLazyFetch<ResourceMeta>(endpoint)
const data = computed(() => {
return resource.value?.results ?? []
})
const columns = ref<ColumnDef<Pokemon>[]>([
{
header: 'Name',
accessorKey: 'name',
},
{
header: 'Url',
accessorKey: 'url',
},
])
const pageCount = computed(() => {
const { pageSize } = pagination.value
return Math.ceil((resource.value?.count || 0) / pageSize)
})
const table = useTemplateRef<Table<Pokemon>>('table')
</script>
<template>
<div class="flex flex-col space-y-4">
<!-- header -->
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div class="flex items-center gap-x-2 sm:ml-auto">
<NButton
:loading="status === 'pending'"
@click="refresh()"
>
Reload
</NButton>
</div>
</div>
<!-- table -->
<NTable
ref="table"
v-model:pagination="pagination"
manual-pagination
:columns
:loading="status === 'pending'"
:page-count
:data
/>
<!-- footer -->
<div
class="flex items-center justify-end px-2"
>
<div class="flex items-center justify-between space-x-6 lg:space-x-8">
<div
class="hidden items-center justify-center text-sm font-medium sm:flex space-x-2"
>
<span class="text-nowrap">
Rows per page
</span>
<NSelect
:items="[5, 10, 20, 30, 40, 50]"
:_select-trigger="{
class: 'w-15',
}"
:model-value="table?.getState().pagination.pageSize"
@update:model-value="table?.setPageSize($event as unknown as number)"
/>
</div>
<div
class="flex items-center justify-center text-sm font-medium"
>
Page {{ (table?.getState().pagination.pageIndex ?? 0) + 1 }} of {{ pageCount }}
</div>
<NPagination
:page="(table?.getState().pagination.pageIndex ?? 0) + 1"
:total="pageCount * pagination.pageSize"
:show-list-item="false"
:items-per-page="table?.getState().pagination.pageSize"
@update:page="table?.setPageIndex($event - 1)"
/>
</div>
</div>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Customization
Configure the progress using the una
prop and utility classes.
Prop | Default | Type | Description |
---|---|---|---|
columns.meta.una | {} | Object | Column Una meta data. |
una | {} | Object | Global Una attribute. |
First Name | Last Name | Age | Visits | Status | Profile Progress |
---|---|---|---|---|---|
Cali | Nikolaus | 0 | 205 | complicated | 92 |
Jacquelyn | Lakin | 24 | 933 | single | 91 |
Chyna | Bechtelar | 13 | 938 | single | 50 |
Leonie | Ratke | 22 | 96 | complicated | 73 |
Joshuah | Gusikowski | 16 | 215 | complicated | 46 |
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { Person } from './makeData'
import makeData from './makeData'
const data = ref(makeData(5))
const columns: ColumnDef<Person>[] = [
{
header: 'First Name',
accessorKey: 'firstName',
meta: {
una: {
tableHead: 'text-left bg-primary-700 text-white',
tableCell: 'text-left bg-primary-700 text-white',
},
},
},
{
header: 'Last Name',
accessorKey: 'lastName',
meta: {
una: {
tableHead: 'text-left bg-primary-700 text-white',
tableCell: 'text-left bg-primary-700 text-white',
},
},
},
{
header: 'Age',
accessorKey: 'age',
meta: {
una: {
tableHead: 'text-center',
tableCell: 'text-center',
},
},
},
{
header: 'Visits',
accessorKey: 'visits',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Profile Progress',
accessorKey: 'progress',
},
]
</script>
<template>
<NTable
:columns
:data
:column-pinning="{
left: ['firstName', 'lastName'],
}"
:una="{
tableHead: 'text-right',
tableCell: 'text-right',
}"
/>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Slots
Name | Props | Description |
---|---|---|
{column}-filter | column | Column filter slot. |
{column}-header | column | Column header slot. |
{column}-cell | cell | Column cell slot. |
{column}-footer | column | Column footer slot. |
header | table | Header slot. |
body | table | Body slot. |
raw | row | Row slot. |
footer | table | Footer slot. |
expanded | row | Expanded slot. |
empty | - | Empty slot. |
loading | - | Loading slot. |
Account | |||||
---|---|---|---|---|---|
BM Bell Murazik Abagail15@yahoo.com | Bell | Murazik | single | 11% | |
PP Palma Prohaska Ford73@yahoo.com | Palma | Prohaska | complicated | 58% | |
ML Marquis Langosh Kattie.Rogahn88@yahoo.com | Marquis | Langosh | relationship | 39% | |
QL Quinton Langosh Hailey.Cummings55@gmail.com | Quinton | Langosh | complicated | 34% | |
GL Guillermo Lang Ignatius_Nolan53@hotmail.com | Guillermo | Lang | relationship | 50% | |
AV Abdullah Von Adela.Stokes@yahoo.com | Abdullah | Von | single | 71% | |
AK Albina Keeling Grayson57@yahoo.com | Albina | Keeling | complicated | 80% | |
AW Amber Willms Samanta_Kihn@gmail.com | Amber | Willms | relationship | 27% | |
MR Mateo Ratke Carolanne39@gmail.com | Mateo | Ratke | complicated | 84% | |
NK Noemie Koelpin Rosanna.Kuhlman@yahoo.com | Noemie | Koelpin | single | 0% | |
<script setup lang="ts">
import type { ColumnDef, Table } from '@tanstack/vue-table'
import { NAvatar } from '#components'
import { faker } from '@faker-js/faker'
import makeData, { type Person } from './makeData'
const data = ref(makeData(50))
const columns: ColumnDef<Person>[] = [
{
header: 'Account',
accessorKey: 'account',
accessorFn: (row) => {
return {
fullname: `${row.firstName} ${row.lastName}`,
avatar: faker.image.avatar(),
email: row.email,
}
},
// you can customize the cell renderer like this as an alternative to slot 😉
cell: (info: any) => {
const fullname = info.getValue().fullname
return h('div', {
class: 'flex items-center',
}, [
h(NAvatar, {
src: info.getValue().avatar,
alt: fullname,
}),
[
h('div', {
class: 'ml-2',
}, [
h('div', {
class: 'text-sm font-semibold leading-none',
}, fullname),
h('span', {
class: 'text-sm text-muted',
}, info.getValue().email),
]),
],
])
},
enableSorting: false,
enableColumnFilter: false,
},
{
header: 'First Name',
accessorKey: 'firstName',
},
{
header: 'Last Name',
accessorKey: 'lastName',
},
{
header: 'Status',
accessorKey: 'status',
},
{
header: 'Progress',
accessorKey: 'progress',
},
]
const search = ref('')
const select = ref()
const table = useTemplateRef<Table<Person>>('table')
</script>
<template>
<div class="flex flex-col space-y-4">
<!-- header -->
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<NInput
v-model="search"
leading="i-radix-icons-magnifying-glass"
placeholder="Search"
:una="{
inputWrapper: 'w-full md:w-80',
}"
/>
<div class="flex items-center gap-x-2 sm:ml-auto">
<NButton
label="Rerender"
btn="solid-gray"
leading="i-radix-icons-update"
class="w-full sm:w-auto sm:shrink-0 active:translate-y-0.5"
@click="data = makeData(20_000)"
/>
<NButton
label="Add 1000"
btn="solid-primary"
leading="i-radix-icons-plus"
class="w-full sm:w-auto sm:shrink-0 active:translate-y-0.5"
@click="data = [...makeData(1_000), ...data]"
/>
</div>
</div>
<!-- table -->
<NTable
ref="table"
v-model="select"
:columns
:data
:global-filter="search"
enable-row-selection enable-column-filters enable-sorting
row-id="username"
>
<!-- filters -->
<template #status-filter="{ column }">
<NSelect
:items="['Relationship', 'Complicated', 'Single']"
placeholder="All"
:model-value="column.getFilterValue()"
@update:model-value="column?.setFilterValue($event)"
/>
</template>
<template #progress-filter="{ column }">
<div class="flex items-center space-x-2">
<NInput
type="number"
placeholder="min"
:model-value="column.getFilterValue()?.[0] ?? ''"
@update:model-value="column?.setFilterValue((old: [number, number]) => [
$event,
old?.[1],
])"
/>
<NInput
type="number"
placeholder="max"
:model-value="column.getFilterValue()?.[1] ?? ''"
@update:model-value="column?.setFilterValue((old: [number, number]) => [
old?.[0],
$event,
])"
/>
</div>
</template>
<!-- end filter -->
<!-- cells -->
<template #status-cell="{ cell }">
<NBadge
:una="{
badgeDefaultVariant: cell.row.original.status === 'relationship'
? 'badge-soft-success' : cell.row.original.status === 'single'
? 'badge-soft-info' : 'badge-soft-warning' }"
class="capitalize"
:label="cell.row.original.status"
/>
</template>
<template #progress-cell="{ cell }">
<div class="flex items-center">
<NProgress
:model-value="cell.row.original.progress"
:una="{
progressRoot: cell.row.original.progress >= 85
? 'progress-success' : cell.row.original.progress >= 70
? 'progress-info' : cell.row.original.progress >= 55
? 'progress-warning' : 'progress-error' }"
/>
<span class="ml-2 text-sm text-muted">{{ cell.row.original.progress }}%</span>
</div>
</template>
<!-- end cell -->
</NTable>
<!-- footer -->
<div
class="flex items-center justify-between px-2"
>
<div
class="hidden text-sm text-muted sm:block"
>
{{ table?.getFilteredSelectedRowModel().rows.length.toLocaleString() }} of
{{ table?.getFilteredRowModel().rows.length.toLocaleString() }} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div
class="hidden items-center justify-center text-sm font-medium sm:flex space-x-2"
>
<span class="text-nowrap">
Rows per page
</span>
<NSelect
:items="[10, 20, 30, 40, 50]"
:_select-trigger="{
class: 'w-15',
}"
:model-value="table?.getState().pagination.pageSize"
@update:model-value="table?.setPageSize($event as unknown as number)"
/>
</div>
<div
class="flex items-center justify-center text-sm font-medium"
>
Page {{ (table?.getState().pagination.pageIndex ?? 0) + 1 }} of
{{ table?.getPageCount().toLocaleString() }}
</div>
<NPagination
:page="(table?.getState().pagination.pageIndex ?? 0) + 1"
:total="table?.getFilteredRowModel().rows.length"
:show-list-item="false"
:items-per-page="table?.getState().pagination.pageSize"
@update:page="table?.setPageIndex($event - 1)"
/>
</div>
</div>
</div>
</template>
import { faker } from '@faker-js/faker'
export interface Person {
id: string
username: string
email: string
firstName?: string
lastName?: string
avatar?: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
function range(len: number) {
const arr: number[] = []
for (let i = 0; i < len; i++)
arr.push(i)
return arr
}
function newPerson(): Person {
return {
id: faker.database.mongodbObjectId(),
username: faker.internet.username(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
avatar: faker.image.avatarGitHub(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}
export default makeData
Presets
type TablePrefix = 'table'
export const staticTable: Record<`${TablePrefix}-${string}` | TablePrefix, string> = {
// config
'table-default-variant': 'table-solid-gray',
'table': '',
// table-root
'table-root-wrapper': 'relative w-full overflow-x-auto overflow-y-hidden border border-base rounded-md',
'table-root': 'w-full caption-bottom text-sm',
'table-body': '[&_tr:last-child]:border-0',
'table-caption': 'mt-4 text-sm text-muted',
// table-head
'table-head': 'h-12 px-4 text-left align-middle font-medium text-muted [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-0.5',
'table-head-pinned': 'sticky bg-base',
'table-head-pinned-left': 'left-0',
'table-head-pinned-right': 'right-0',
// table-header
'table-header': '[&_tr]:border-b [&_tr]:border-base',
// table-row
'table-row': 'border-b border-base transition-colors hover:bg-muted data-[filter=true]:hover:bg-base data-[state=selected]:bg-muted',
// table-cell
'table-cell': 'p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-0.5',
'table-cell-pinned': 'sticky bg-base',
'table-cell-pinned-left': 'left-0',
'table-cell-pinned-right': 'right-0',
// table-empty
'table-empty-row': '',
'table-empty-cell': 'p-4 whitespace-nowrap align-middle text-sm text-muted bg-base',
'table-empty': 'flex items-center justify-center py-10',
// table-loading
'table-loading-icon': 'animate-spin text-lg', // TODO: to add
'table-loading-icon-name': 'i-lucide-refresh-ccw', // TODO: to add
'table-loading-row': 'data-[loading=true]:border-0 absolute inset-x-0 -mt-1.5px',
'table-loading-cell': '',
'table-loading': 'absolute inset-x-0 overflow-hidden p-0',
// table-footer
'table-footer': 'border-t border-base bg-muted font-medium [&>tr]:last:border-b-0',
}
export const dynamicTable: [RegExp, (params: RegExpExecArray) => string][] = [
]
export const table = [
...dynamicTable,
staticTable,
]
Props
import type {
ColumnDef,
GroupColumnDef,
} from '@tanstack/vue-table'
import type { HTMLAttributes } from 'vue'
import type { NProgressProps } from './progress'
export interface NTableProps<TData, TValue> extends NTableRootProps {
/**
* @see https://tanstack.com/table/latest/docs/guide/data
*/
data: TData[]
/**
* @see https://tanstack.com/table/latest/docs/api/core/column
*/
columns: ColumnDef<TData, TValue>[] | GroupColumnDef<TData, TValue>[]
/**
* @see https://tanstack.com/table/latest/docs/api/core/table#getrowid
*/
rowId?: string
/**
* @see https://tanstack.com/table/latest/docs/api/core/table#autoresetall
*/
autoResetAll?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/row-selection#enablerowselection
*/
enableRowSelection?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/row-selection#enablemultirowselection
*/
enableMultiRowSelection?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/row-selection#enablesubrowselection
*/
enableSubRowSelection?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/column-filtering#enablecolumnfilters
*/
enableColumnFilters?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#enablesorting
*/
enableSorting?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#enablemultisort
*/
enableMultiSort?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#enablemultiremove
*/
enableMultiRemove?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#enablesortingremoval
*/
enableSortingRemoval?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#manualsorting
*/
manualSorting?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#maxmultisortcolcount
*/
maxMultiSortColCount?: number
/**
* @see https://tanstack.com/table/latest/docs/api/features/pagination#manualpagination
*/
manualPagination?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/pagination#pagecount
*/
pageCount?: number
/**
* @see https://tanstack.com/table/latest/docs/api/features/pagination#rowcount
*/
rowCount?: number
/**
* @see https://tanstack.com/table/latest/docs/api/features/pagination#autoresetpageindex
*/
autoResetPageIndex?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#sortingfns
*/
sortingFns?: Record<string, (a: any, b: any) => number>
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#sortdescfirst-1
*/
sortDescFirst?: boolean
/**
* @see https://tanstack.com/table/latest/docs/api/features/sorting#ismultisortevent
*/
isMultiSortEvent?: (e: unknown) => boolean
// sub-components props
_tableHead?: NTableHeadProps
_tableHeader?: NTableHeaderProps
_tableFooter?: NTableFooterProps
_tableBody?: NTableBodyProps
_tableCaption?: NTableCaptionProps
_tableRow?: NTableRowProps
_tableCell?: NTableCellProps
_tableEmpty?: NTableEmptyProps
_tableLoading?: NTableLoadingProps
loading?: boolean
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/table.ts
*/
una?: NTableUnaProps
}
export interface NTableRootProps {
class?: HTMLAttributes['class']
una?: Pick<NTableUnaProps, 'tableRoot' | 'tableRootWrapper'>
}
export interface NTableBodyProps {
class?: HTMLAttributes['class']
una?: Pick<NTableUnaProps, 'tableBody'>
}
export interface NTableHeadProps {
class?: HTMLAttributes['class']
dataPinned?: 'left' | 'right' | false
una?: Pick<NTableUnaProps, 'tableHead'>
}
export interface NTableHeaderProps {
class?: HTMLAttributes['class']
una?: Pick<NTableUnaProps, 'tableHeader'>
}
export interface NTableFooterProps {
class?: HTMLAttributes['class']
una?: Pick<NTableUnaProps, 'tableFooter'>
}
export interface NTableRowProps {
class?: HTMLAttributes['class']
una?: Pick<NTableUnaProps, 'tableRow'>
}
export interface NTableCellProps {
class?: HTMLAttributes['class']
dataPinned?: 'left' | 'right' | false
una?: Pick<NTableUnaProps, 'tableCell'>
}
export interface NTableEmptyProps {
class?: HTMLAttributes['class']
colspan?: number
_tableCell?: NTableCellProps
_tableRow?: NTableRowProps
una?: Pick<NTableUnaProps, 'tableEmpty' | 'tableRow' | 'tableCell'>
}
export interface NTableLoadingProps {
size?: HTMLAttributes['class']
enabled?: boolean
class?: HTMLAttributes['class']
colspan?: number
_tableCell?: NTableCellProps
_tableRow?: NTableRowProps
_tableProgress?: NProgressProps
una?: Pick<NTableUnaProps, 'tableLoading' | 'tableRow' | 'tableCell'>
}
export interface NTableCaptionProps {
class?: HTMLAttributes['class']
una?: Pick<NTableUnaProps, 'tableCaption'>
}
interface NTableUnaProps {
tableRoot?: HTMLAttributes['class']
tableRootWrapper?: HTMLAttributes['class']
tableBody?: HTMLAttributes['class']
tableHead?: HTMLAttributes['class']
tableHeader?: HTMLAttributes['class']
tableFooter?: HTMLAttributes['class']
tableRow?: HTMLAttributes['class']
tableCell?: HTMLAttributes['class']
tableCaption?: HTMLAttributes['class']
tableEmpty?: HTMLAttributes['class']
tableLoading?: HTMLAttributes['class']
}
Components
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnFiltersState,
ColumnOrderState,
ColumnPinningState,
ExpandedState,
GroupingState,
Header,
PaginationState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import type { NTableProps } from '../../../types'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { computed, h } from 'vue'
import { cn, valueUpdater } from '../../../utils'
import Button from '../../elements/Button.vue'
import Checkbox from '../../forms/Checkbox.vue'
import Input from '../../forms/Input.vue'
import TableBody from './TableBody.vue'
import TableCell from './TableCell.vue'
import TableEmpty from './TableEmpty.vue'
import TableFooter from './TableFooter.vue'
import TableHead from './TableHead.vue'
import TableHeader from './TableHeader.vue'
import TableLoading from './TableLoading.vue'
import TableRoot from './TableRoot.vue'
import TableRow from './TableRow.vue'
const props = withDefaults(defineProps <NTableProps<TData, TValue>>(), {
enableMultiRowSelection: true,
})
const emit = defineEmits(['select', 'selectAll', 'expand'])
const slots = defineSlots()
const rowSelection = defineModel<Record<string, boolean>>('modelValue')
const sorting = defineModel<SortingState>('sorting')
const columnVisibility = defineModel<VisibilityState>('columnVisibility')
const columnFilters = defineModel<ColumnFiltersState>('columnFilters')
const globalFilter = defineModel<string>('globalFilter')
const columnOrder = defineModel<ColumnOrderState>('columnOrder')
const columnPinning = defineModel<ColumnPinningState>('columnPinning')
const expanded = defineModel<ExpandedState>('expanded')
const grouping = defineModel<GroupingState>('grouping')
const pagination = defineModel<PaginationState>('pagination', {
default: () => ({
pageIndex: 0,
pageSize: 10,
}),
})
const columnsWithMisc = computed(() => {
let data = []
// add selection column
data = props.enableRowSelection
? [
{
accessorKey: 'selection',
header: props.enableMultiRowSelection
? ({ table }: any) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:checked': (value: boolean) => {
table.toggleAllPageRowsSelected(!!value)
emit('selectAll', table.getRowModel().rows)
},
'areaLabel': 'Select all rows',
})
: '',
cell: ({ row }: any) => h(Checkbox, {
'checked': row.getIsSelected() ?? false,
'onUpdate:checked': (value: boolean) => {
row.toggleSelected(!!value)
emit('select', row)
},
'areaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
...props.columns,
]
: props.columns
// add expanded column
data = slots.expanded
? [
{
accessorKey: 'expanded',
header: '',
cell: ({ row }: any) => h(Button, {
size: 'xs',
icon: true,
square: true,
btn: 'ghost-gray',
label: 'i-radix-icons-chevron-down',
onClick: () => {
row.toggleExpanded()
emit('expand', row)
},
una: {
btnIconLabel: cn(
'transform transition-transform duration-200',
row.getIsExpanded() ? '-rotate-180' : 'rotate-0',
),
},
}),
enableSorting: false,
enableHiding: false,
},
...data,
]
: data
return data
})
const table = useVueTable({
get data() {
return props.data ?? []
},
get columns() {
return columnsWithMisc.value ?? []
},
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get globalFilter() { return globalFilter.value },
get rowSelection() { return rowSelection.value },
get columnVisibility() { return columnVisibility.value },
get pagination() { return pagination.value },
get columnOrder() { return columnOrder.value },
get columnPinning() { return columnPinning.value },
get expanded() { return expanded.value },
get grouping() { return grouping.value },
},
enableMultiRowSelection: props.enableMultiRowSelection,
enableSubRowSelection: props.enableSubRowSelection,
autoResetAll: props.autoResetAll,
enableRowSelection: props.enableRowSelection,
enableColumnFilters: props.enableColumnFilters,
manualPagination: props.manualPagination,
manualSorting: props.manualSorting,
pageCount: props.pageCount,
rowCount: props.rowCount,
autoResetPageIndex: props.autoResetPageIndex,
enableSorting: props.enableSorting,
enableSortingRemoval: props.enableSortingRemoval,
enableMultiSort: props.enableMultiSort,
enableMultiRemove: props.enableMultiRemove,
maxMultiSortColCount: props.maxMultiSortColCount,
sortingFns: props.sortingFns,
isMultiSortEvent: props.isMultiSortEvent,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getRowId: (row: any) => props.rowId ? row[props.rowId] : row.id,
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onGlobalFilterChange: updaterOrValue => valueUpdater(updaterOrValue, globalFilter),
onPaginationChange: updaterOrValue => valueUpdater(updaterOrValue, pagination),
onColumnOrderChange: updaterOrValue => valueUpdater(updaterOrValue, columnOrder),
onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinning),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
onGroupingChange: updaterOrValue => valueUpdater(updaterOrValue, grouping),
})
function getHeaderColumnFiltersCount(headers: Header<unknown, unknown>[]): number {
let count = 0
headers.forEach((header) => {
if (header.column.columnDef.enableColumnFilter)
count++
})
return count
}
defineExpose({
...table,
})
</script>
<template>
<TableRoot
:class="props.class"
:una
>
<!-- header -->
<TableHeader
:una
v-bind="props._tableHeader"
>
<slot name="header" :table="table">
<TableRow
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
:una
v-bind="props._tableRow"
>
<!-- headers -->
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
:colspan="header.colSpan"
:data-pinned="header.column.getIsPinned()"
:una
v-bind="{ ...props._tableHead, ...header.column.columnDef.meta }"
>
<Button
v-if="header.column.columnDef.enableSorting || (header.column.columnDef.enableSorting !== false && enableSorting)"
btn="ghost-gray"
size="sm"
class="font-normal -ml-1em"
:una="{
btnTrailing: 'text-sm',
}"
:trailing="header.column.getIsSorted() === 'asc'
? 'i-lucide-arrow-up-wide-narrow' : header.column.getIsSorted() === 'desc'
? 'i-lucide-arrow-down-narrow-wide' : 'i-lucide-arrow-up-down'"
@click="header.column.toggleSorting(
header.column.getIsSorted() === 'asc' ? undefined : header.column.getIsSorted() !== 'desc',
enableMultiSort,
)"
>
<slot
:name="`${header.id}-header`"
:column="header.column"
>
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</slot>
</Button>
<slot
v-else
:name="`${header.id}-header`"
:column="header.column"
>
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</slot>
</TableHead>
</TableRow>
<!-- column filters -->
<template
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<TableRow
v-if="getHeaderColumnFiltersCount(headerGroup.headers) > 0 || enableColumnFilters"
data-filter="true"
:una
v-bind="props._tableRow"
>
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
:una
:colspan="header.colSpan"
:data-pinned="header.column.getIsPinned()"
v-bind="{ ...props._tableHead, ...header.column.columnDef.meta }"
>
<slot
v-if="header.id !== 'selection' && ((header.column.columnDef.enableColumnFilter !== false && enableColumnFilters) || header.column.columnDef.enableColumnFilter)"
:name="`${header.id}-filter`"
:column="header.column"
>
<Input
class="w-auto text-sm text-base"
:model-value="header.column.getFilterValue() as string"
:placeholder="header.column.columnDef.header"
@update:model-value="header.column.setFilterValue($event)"
/>
</slot>
</TableHead>
</TableRow>
</template>
</slot>
<TableLoading
:enabled="props.loading"
:una
v-bind="props._tableLoading"
>
<slot name="loading" />
</TableLoading>
</TableHeader>
<!-- body -->
<TableBody
:una
v-bind="props._tableBody"
>
<slot name="body" :table="table">
<template v-if="table.getRowModel().rows?.length">
<template
v-for="row in table.getRowModel().rows"
:key="row.id"
>
<TableRow
:data-state="row.getIsSelected() && 'selected'"
:una
v-bind="props._tableRow"
>
<slot
name="row"
:row="row"
>
<!-- rows -->
<TableCell
v-for="cell in row.getVisibleCells()"
:key="cell.id"
:data-pinned="cell.column.getIsPinned()"
:una
v-bind="{ ...props._tableCell, ...cell.column.columnDef.meta }"
>
<slot
:name="`${cell.column.id}-cell`"
:cell="cell"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</slot>
</TableCell>
</slot>
</TableRow>
<!-- expanded -->
<TableRow
v-if="row.getIsExpanded() && $slots.expanded"
:una
v-bind="props._tableRow"
>
<TableCell
:colspan="row.getAllCells().length"
:una
v-bind="props._tableCell"
>
<slot name="expanded" :row="row" />
</TableCell>
</TableRow>
</template>
</template>
<TableEmpty
v-else
:colspan="table.getAllLeafColumns().length"
:una
v-bind="props._tableEmpty"
>
<slot name="empty" />
</TableEmpty>
</slot>
</TableBody>
<!-- footer -->
<TableFooter
v-if="table.getFooterGroups().length > 0"
:una
v-bind="props._tableFooter"
>
<slot name="footer" :table="table">
<template
v-for="footerGroup in table.getFooterGroups()"
:key="footerGroup.id"
>
<TableRow
v-if="footerGroup.headers.length > 0"
:una
v-bind="props._tableRow"
>
<template
v-for="header in footerGroup.headers"
:key="header.id"
>
<TableHead
v-if="header.column.columnDef.footer"
:colspan="header.colSpan"
:una
v-bind="{ ...props._tableHead, ...header.column.columnDef.meta }"
>
<slot :name="`${header.id}-footer`" :column="header.column">
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.footer"
:props="header.getContext()"
/>
</slot>
</TableHead>
</template>
</TableRow>
</template>
</slot>
</TableFooter>
</TableRoot>
</template>
<script setup lang="ts">
import type { NTableRootProps } from '../../../types'
import { cn } from '../../../utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<NTableRootProps>()
</script>
<template>
<div
:class="cn('table-root-wrapper', props.una?.tableRootWrapper)"
>
<table
v-bind="$attrs"
:class="cn(
'table-root',
props.una?.tableRoot,
props.class,
)"
>
<slot />
</table>
</div>
</template>
<script setup lang="ts">
import type { NTableHeaderProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableHeaderProps>()
</script>
<template>
<thead
:class="cn(
'table-header',
props?.una?.tableHeader,
props.class,
)"
v-bind="$attrs"
>
<slot />
</thead>
</template>
<script setup lang="ts">
import type { NTableHeadProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableHeadProps>()
</script>
<template>
<th
:class="cn(
'table-head',
props.una?.tableHead,
props.class,
{ 'table-head-pinned': props.dataPinned },
props.dataPinned === 'left' ? 'table-head-pinned-left' : 'table-head-pinned-right',
)"
v-bind="$attrs"
>
<slot />
</th>
</template>
<script setup lang="ts">
import type { NTableBodyProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableBodyProps>()
</script>
<template>
<tbody
:class="cn(
'table-body',
props?.una?.tableBody,
props.class,
)"
v-bind="$attrs"
>
<slot />
</tbody>
</template>
<script setup lang="ts">
import type { NTableFooterProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableFooterProps>()
</script>
<template>
<tfoot
:class="cn(
'table-footer',
props.una?.tableFooter,
props.class,
)"
>
<slot />
</tfoot>
</template>
<script setup lang="ts">
import type { NTableCellProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableCellProps>()
</script>
<template>
<td
:class="
cn(
'table-cell',
props?.una?.tableCell,
props.class,
{ 'table-cell-pinned': dataPinned },
dataPinned === 'left' ? 'table-cell-pinned-left' : 'table-cell-pinned-right',
)
"
v-bind="$attrs"
>
<slot />
</td>
</template>
<script setup lang="ts">
import type { NTableRowProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableRowProps>()
</script>
<template>
<tr
:class="cn(
'table-row',
props.una?.tableRow,
props.class,
)"
v-bind="$attrs"
>
<slot />
</tr>
</template>
<script setup lang="ts">
import type { NTableEmptyProps } from '../../../types'
import { computed } from 'vue'
import { cn, omitProps } from '../../../utils'
import TableCell from './TableCell.vue'
import TableRow from './TableRow.vue'
const props = withDefaults(defineProps<NTableEmptyProps>(), {
colspan: 1,
})
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<TableRow
:class="cn(
'table-empty-row',
props.una?.tableRow,
)"
v-bind="delegatedProps._tableRow"
>
<TableCell
:class="
cn(
'table-empty-cell',
props.una?.tableCell,
)
"
:colspan="props.colspan"
v-bind="delegatedProps._tableCell"
>
<div
:class="cn(
'table-empty',
props.una?.tableEmpty,
props.class,
)"
v-bind="omitProps(delegatedProps, ['_tableRow', '_tableCell', 'colspan'])"
>
<slot>
<div class="grid place-items-center gap-4">
<NIcon
name="i-tabler-database-x"
size="2xl"
/>
<span>
No results.
</span>
</div>
</slot>
</div>
</TableCell>
</TableRow>
</template>
<script setup lang="ts">
import type { NTableLoadingProps } from '../../../types'
import { computed } from 'vue'
import { cn } from '../../../utils'
import Progress from '../../elements/Progress.vue'
import TableRow from './TableRow.vue'
const props = withDefaults(defineProps<NTableLoadingProps>(), {
size: '2.5px',
})
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<TableRow
:class="cn(
'table-loading-row',
props.una?.tableRow,
)"
data-loading="true"
v-bind="delegatedProps._tableRow"
>
<td
:class="
cn(
'table-loading-cell',
props.una?.tableCell,
)
"
:colspan="0"
v-bind="delegatedProps._tableCell"
>
<div
v-if="enabled"
:class="cn(
'table-loading',
props.una?.tableLoading,
)"
>
<slot>
<Progress
:size
v-bind="props._tableProgress"
:class="cn(
props._tableProgress?.class,
)"
/>
</slot>
</div>
</td>
</TableRow>
</template>
<script setup lang="ts">
import type { NTableCaptionProps } from '../../../types'
import { cn } from '../../../utils'
const props = defineProps<NTableCaptionProps>()
</script>
<template>
<caption
:class="cn(
'table-caption',
props?.una?.tableCaption,
props.class,
)"
v-bind="$attrs"
>
<slot />
</caption>
</template>