
Web developers often face the challenge of delivering high-quality code quickly in today's fast-paced world. However, they can streamline development by leveraging existing frameworks and libraries instead of reinventing the wheel.
We will dive into the process of developing a table with sort and pagination features using Vite, Tailwind CSS, and the powerful TanStack Table.
Initializing the Vite Project
If you already have a base project, go directly to Initializing Table development
To create a new project, execute the following command in your terminal:
yarn create vite
You'll then be prompted to configure the Vite App:
√ Project name: … tanstack-table-vite
√ Select a framework: » React
√ Select a variant: » TypeScript
Access your project folder using the command:
cd tanstack-table-vite
Installing Dependencies
Install the TanStack Table library by executing the following command within your project directory:
yarn add @tanstack/react-table
Install and create files for Tailwind CSS by executing these commands:
yarn add -D tailwindcss postcss autoprefixer
yarn tailwind init -p
In your tailwind.config.js
file, replace the existing code with the following:
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
Open the index.css
file, delete all the CSS code, and paste the following:
@tailwind base;
@tailwind components;
@tailwind utilities;
Initialize the development application:
yarn run dev
Initializing Table development
For demonstration purposes, we'll employ mock data in our table. Create a new TypeScript file, data.json
, and define your mock data:
[
{
"id": 1,
"name": "John Doe",
"email": "johndoe@example.com",
"phone": "123-456-7890"
},
{
"id": 2,
"name": "Jane Smith",
"email": "janesmith@example.com",
"phone": "987-654-3210"
},
{
"id": 3,
"name": "Michael Johnson",
"email": "michaeljohnson@example.com",
"phone": "555-123-4567"
},
{
"id": 4,
"name": "Emily Wilson",
"email": "emilywilson@example.com",
"phone": "999-888-7777"
},
{
"id": 5,
"name": "Daniel Lee",
"email": "daniellee@example.com",
"phone": "444-555-6666"
},
{
"id": 6,
"name": "Olivia Martinez",
"email": "oliviamartinez@example.com",
"phone": "777-999-1111"
},
{
"id": 7,
"name": "William Thompson",
"email": "williamthompson@example.com",
"phone": "222-333-4444"
}
]
In your App.tsx
file, delete all its previous content and replace it with the following code:
import * as React from 'react';
import mockData from './data.json';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
type Person = {
id: number;
name: string;
email: string;
phone: string;
};
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor('id', {
cell: (info) => info.getValue(),
}),
columnHelper.accessor('name', {
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => row.email, {
id: 'email',
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Email</span>,
}),
columnHelper.accessor('phone', {
header: () => 'Phone',
cell: (info) => info.renderValue(),
}),
];
export default function App() {
const [data] = React.useState(() => [...mockData]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="flex justify-center h-screen">
<table className="my-auto border">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr
key={headerGroup.id}
className="border-b text-gray-800 uppercase"
>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-4 pr-2 py-4 font-medium text-left"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-b">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-4 pt-[14px] pb-[18px]">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div />
</div>
);
}
Now, our app looks like this on browser:

Implementing Sorting powers
Import two new functions, getSortedRowModel
and SortingState
, from @tanstack/react-table
.
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
// new imports:
getSortedRowModel,
SortingState,
} from '@tanstack/react-table';
Add a new state, sorting
, and update the table
constant to include sorting-related configurations:
const [sorting, setSorting] = React.useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: {
sorting,
},
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
});
Update the <thead>
tag in the code snippet to enable sorting functionality.
We're adding the function to sort and arrows to indicate the direction of the sorting, feel free to change the icon and styling:
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="border-b text-gray-800 uppercase">
{headerGroup.headers.map((header) => (
<th key={header.id} className="px-4 pr-2 py-4 font-medium text-left">
{header.isPlaceholder ? null : (
<div
{...{
className: header.column.getCanSort()
? 'cursor-pointer select-none flex min-w-[36px]'
: '',
onClick: header.column.getToggleSortingHandler(),
}}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: <span className="pl-2">↑</span>,
desc: <span className="pl-2">↓</span>,
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
</th>
))}
</tr>
))}
</thead>
Our updated table is now equipped with enhanced functionality that allows for seamless sorting of columns in both ascending and descending orders. By using TanStack Table, the process is streamlined significantly, eliminating the need for manual sorting and ensuring a smoother user experience.
Implementing Pagination powers
Import the getPaginationRowModel
function from @tanstack/react-table
.
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
SortingState,
// new import:
getPaginationRowModel,
} from '@tanstack/react-table';
Update the table
constant to include pagination-related configurations (items per page and initializing its functions):
const table = useReactTable({
data,
columns,
state: {
sorting,
},
// initial items per page
initialState: {
pagination: {
pageSize: 2,
},
},
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
// initialize pagination function
getPaginationRowModel: getPaginationRowModel(),
});
Add the necessary elements and functions after the closing </table>
tag to enable pagination.
<div className="flex sm:flex-row flex-col w-full mt-8 items-center gap-2 text-xs">
<div className="sm:mr-auto sm:mb-0 mb-2">
<span className="mr-2">Items por página</span>
<select
className="border p-1 rounded w-16 border-gray-200"
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[2, 4, 6, 8].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<div className="flex gap-2">
<button
className={`${
!table.getCanPreviousPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="w-5 h-5">{'<<'}</span>
</button>
<button
className={`${
!table.getCanPreviousPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="w-5 h-5">{'<'}</span>
</button>
<span className="flex items-center gap-1">
<input
min={1}
max={table.getPageCount()}
type="number"
value={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
className="border p-1 rounded w-10"
/>
de {table.getPageCount()}
</span>
<button
className={`${
!table.getCanNextPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="w-5 h-5">{'>'}</span>
</button>
<button
className={`${
!table.getCanNextPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="w-5 h-5">{'>>'}</span>
</button>
</div>
</div>
Now your table is complete with sort and pagination features using Vite, Tailwind, and TanStack Table.
Our final code looks like that now:
import * as React from 'react';
import mockData from './data.json';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
SortingState,
getPaginationRowModel,
} from '@tanstack/react-table';
type Person = {
id: number;
name: string;
email: string;
phone: string;
};
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor('id', {
cell: (info) => info.getValue(),
}),
columnHelper.accessor('name', {
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => row.email, {
id: 'email',
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Email</span>,
}),
columnHelper.accessor('phone', {
header: () => 'Phone',
cell: (info) => info.renderValue(),
}),
];
export default function App() {
const [data] = React.useState(() => [...mockData]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: {
sorting,
},
initialState: {
pagination: {
pageSize: 2,
},
},
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto py-24">
<table className="border">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr
key={headerGroup.id}
className="border-b text-gray-800 uppercase"
>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-4 pr-2 py-4 font-medium text-left"
>
{header.isPlaceholder ? null : (
<div
{...{
className: header.column.getCanSort()
? 'cursor-pointer select-none flex min-w-[36px]'
: '',
onClick: header.column.getToggleSortingHandler(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: <span className="pl-2">↑</span>,
desc: <span className="pl-2">↓</span>,
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-b">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-4 pt-[14px] pb-[18px]">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="flex sm:flex-row flex-col w-full mt-8 items-center gap-2 text-xs">
<div className="sm:mr-auto sm:mb-0 mb-2">
<span className="mr-2">Items por página</span>
<select
className="border p-1 rounded w-16 border-gray-200"
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[2, 4, 6, 8].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<div className="flex gap-2">
<button
className={`${
!table.getCanPreviousPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="w-5 h-5">{'<<'}</span>
</button>
<button
className={`${
!table.getCanPreviousPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="w-5 h-5">{'<'}</span>
</button>
<span className="flex items-center gap-1">
<input
min={1}
max={table.getPageCount()}
type="number"
value={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
className="border p-1 rounded w-10"
/>
de {table.getPageCount()}
</span>
<button
className={`${
!table.getCanNextPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="w-5 h-5">{'>'}</span>
</button>
<button
className={`${
!table.getCanNextPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="w-5 h-5">{'>>'}</span>
</button>
</div>
</div>
</div>
);
}
Now your table is complete with sort and pagination features using Vite, Tailwind, and TanStack Table.
Our final code looks like that now:
import * as React from 'react';
import mockData from './data.json';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getSortedRowModel,
SortingState,
getPaginationRowModel,
} from '@tanstack/react-table';
type Person = {
id: number;
name: string;
email: string;
phone: string;
};
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor('id', {
cell: (info) => info.getValue(),
}),
columnHelper.accessor('name', {
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => row.email, {
id: 'email',
cell: (info) => <i>{info.getValue()}</i>,
header: () => <span>Email</span>,
}),
columnHelper.accessor('phone', {
header: () => 'Phone',
cell: (info) => info.renderValue(),
}),
];
export default function App() {
const [data] = React.useState(() => [...mockData]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: {
sorting,
},
initialState: {
pagination: {
pageSize: 2,
},
},
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto py-24">
<table className="border">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr
key={headerGroup.id}
className="border-b text-gray-800 uppercase"
>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-4 pr-2 py-4 font-medium text-left"
>
{header.isPlaceholder ? null : (
<div
{...{
className: header.column.getCanSort()
? 'cursor-pointer select-none flex min-w-[36px]'
: '',
onClick: header.column.getToggleSortingHandler(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: <span className="pl-2">↑</span>,
desc: <span className="pl-2">↓</span>,
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-b">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-4 pt-[14px] pb-[18px]">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="flex sm:flex-row flex-col w-full mt-8 items-center gap-2 text-xs">
<div className="sm:mr-auto sm:mb-0 mb-2">
<span className="mr-2">Items por página</span>
<select
className="border p-1 rounded w-16 border-gray-200"
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[2, 4, 6, 8].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<div className="flex gap-2">
<button
className={`${
!table.getCanPreviousPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="w-5 h-5">{'<<'}</span>
</button>
<button
className={`${
!table.getCanPreviousPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="w-5 h-5">{'<'}</span>
</button>
<span className="flex items-center gap-1">
<input
min={1}
max={table.getPageCount()}
type="number"
value={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
className="border p-1 rounded w-10"
/>
de {table.getPageCount()}
</span>
<button
className={`${
!table.getCanNextPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="w-5 h-5">{'>'}</span>
</button>
<button
className={`${
!table.getCanNextPage()
? 'bg-gray-100'
: 'hover:bg-gray-200 hover:curstor-pointer bg-gray-100'
} rounded p-1`}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="w-5 h-5">{'>>'}</span>
</button>
</div>
</div>
</div>
);
}
Remember that TanStack Table is a Headless UI library, which means it provides functionality without styling. You can apply your own design system to customize its appearance. For more features, refer to the TanStack React examples.
Full code here: https://github.com/jordam1407/tanstack-table
LinkedIn: https://www.linkedin.com/in/jordammendes/