---
title: Data Table
description: Powerful table and datagrids built using TanStack Table.
primitive: https://tanstack.com/table/v8/docs/guide/introduction
---
## Introduction
Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/v8/docs/guide/introduction#what-is-headless-ui) provides.
So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.
We'll start with the basic `
` component and build a complex data table from scratch.
**Tip:** If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.
## Table of Contents
This guide will show you how to use [TanStack Table](https://tanstack.com/table/v8) and the `
` component to build your own custom data table. We'll cover the following topics:
- [Basic Table](#basic-table)
- [Row Actions](#row-actions)
- [Pagination](#pagination)
- [Sorting](#sorting)
- [Filtering](#filtering)
- [Visibility](#visibility)
- [Row Selection](#row-selection)
- [Reusable Components](#reusable-components)
## Installation
1. Add the `
` component to your project:
```bash
npx shadcn-vue@latest add table
```
2. Add `tanstack/vue-table` dependency:
```bash
npm install @tanstack/vue-table
```
## Examples
### Column Pinning
## Prerequisites
We are going to build a table to show recent payments. Here's what our data looks like:
```ts:line-numbers
interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
export const payments: Payment[] = [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: 'm@example.com',
},
{
id: '489e1d42',
amount: 125,
status: 'processing',
email: 'example@gmail.com',
},
// ...
]
```
## Project Structure
Start by creating the following file structure:
```txt
components
└── payments
├── columns.ts
├── data-table.vue
├── data-table-dropdown.vue
└── app.vue
```
I'm using a Nuxt example here but this works for any other Vue framework.
- `columns.ts` It will contain our column definitions.
- `data-table.vue` It will contain our `` component.
- `data-table-dropdown.vue` It will contain our `` component.
- `app.vue` This is where we'll fetch data and render our table.
## Basic Table
Let's start by building a basic table.
### Column Definitions
First, we'll define our columns in the `columns.ts` file.
```ts:line-numbers
import { h } from 'vue'
export const columns: ColumnDef[] = [
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
}
]
```
**Note:** Columns are where you define the core of what your table
will look like. They define the data that will be displayed, how it will be
formatted, sorted and filtered.
### `` component
Next, we'll create a `` component to render our table.
```vue
No results.
```
**Tip**: If you find yourself using `` in multiple places, this is the component you could make reusable by extracting it to `components/ui/data-table.vue`.
``
### Render the table
Finally, we'll render our table in our index component.
```vue
```
## Cell Formatting
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
### Update columns definition
Update the `header` and `cell` definitions for amount as follows:
```ts
import { h } from 'vue'
export const columns: ColumnDef[] = [
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
}
]
```
You can use the same approach to format other cells and headers.
## Row Actions
Let's add row actions to our table. We'll use a `` component for this.
### Add the following into your `DataTableDropDown.vue` component
```vue
Actions
Copy payment ID
View customerView payment details
```
### Update columns definition
Update our columns definition to add a new `actions` column. The `actions` cell returns a `` component.
```ts
import { ColumnDef } from '@tanstack/vue-table'
import DropdownAction from '@/components/DataTableDropDown.vue'
export const columns: ColumnDef[] = [
// ...
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
}))
},
},
]
```
You can access the row data using `row.original` in the `cell` function. Use this to handle actions for your row eg. use the `id` to make a DELETE call to your API.
## Pagination
Next, we'll add pagination to our table.
### Update ``
```ts:line-numbers {4,12}
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
useVueTable,
} from "@tanstack/vue-table"
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
```
This will automatically paginate your rows into pages of 10. See the [pagination docs](https://tanstack.com/table/v8/docs/api/features/pagination) for more information on customizing page size and implementing manual pagination.
### Add pagination controls
We can add pagination controls to our table using the `` component and the `table.previousPage()`, `table.nextPage()` API methods.
```vue
{ // .... }
```
See [Reusable Components](#reusable-components) section for a more advanced pagination component.
## Sorting
Let's make the email column sortable.
### Add the following into your `utils` file
```ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import type { Updater } from '@tanstack/vue-table'
import type { Ref } from 'vue'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function valueUpdater>(updaterOrValue: T, ref: Ref) {
ref.value = typeof updaterOrValue === 'function'
? updaterOrValue(ref.value)
: updaterOrValue
}
```
The `valueUpdater` function updates a Vue `ref` object's value. It handles both direct assignments and transformations using a function. If `updaterOrValue` is a function, it's called with the current `ref` value, and the result is assigned to `ref.value`. If it's not a function, it's directly assigned to `ref.value`. This utility enhances flexibility in updating `ref` values. While Vue `ref` can manage reactive state directly, `valueUpdater` simplifies value updates, improving code readability and maintainability when the new state can be a direct value or a function generating it based on the current one.
### Update ``
```vue:line-numbers {4,14,17,33,40-44}
{ ... }
```
### Make header cell sortable
We can now update the `email` header cell to add sorting controls.
```ts:line-numbers {5,10-17}
// components/payments/columns.ts
import type {
ColumnDef,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
export const columns: ColumnDef[] = [
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
]
```
This will automatically sort the table (asc and desc) when the user toggles on the header cell.
## Filtering
Let's add a search input to filter emails in our table.
### Update ``
```vue:line-numbers {4,11,19,39,48-49,52,60-64}
{ ... }
```
Filtering is now enabled for the `email` column. You can add filters to other columns as well. See the [filtering docs](https://tanstack.com/table/v8/docs/guide/filters) for more information on customizing filters.
## Visibility
Adding column visibility is fairly simple using `@tanstack/vue-table` visibility API.
### Update ``
```vue:line-numbers {6,9-14,48,59,63,75-91}
```
This adds a dropdown menu that you can use to toggle column visibility.
## Row Selection
Next, we're going to add row selection to our table.
### Update column definitions
```ts:line-numbers {3,6-20}
import type { ColumnDef } from '@tanstack/vue-table'
import { Checkbox } from '@/components/ui/checkbox'
export const columns: ColumnDef[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'checked': table.getIsAllPageRowsSelected(),
'onUpdate:checked': (value: boolean) => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'checked': row.getIsSelected(),
'onUpdate:checked': (value: boolean) => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
]
```
### Update ``
```vue:line-numbers {10,22,27}
```
This adds a checkbox to each row and a checkbox in the header to select all rows.
### Show selected rows
You can show the number of selected rows using the `table.getFilteredSelectedRowModel()` API.
```vue
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
```
## Reusable Components
Here are some components you can use to build your data tables. This is from the [Tasks](/examples/tasks) demo.
### Column header
Make any column header sortable and hideable.
```vue:line-numbers