Tanstack Table Design Using React js and tailwind css search, pagination, download option
Data tables are essential components in modern web applications, but creating feature-rich, performant tables from scratch can be challenging. In this comprehensive tutorial, we'll build a powerful data table using TanStack Table (formerly React Table) with React and Tailwind CSS that includes search functionality, pagination, and Excel export capabilities.
What We're Building
By the end of this tutorial, you'll have a fully functional data table with:
Global search across all columns
Pagination with customizable page sizes
Excel export functionality
Responsive design with Tailwind CSS
Debounced search input for better performance
Professional styling with dark theme
Show Image
Why TanStack Table?
TanStack Table is a headless UI library that provides:
Framework agnostic - works with React, Vue, Svelte, and more
Lightweight - only includes what you need
Highly performant - optimized for large datasets
Fully customizable - complete control over styling and behavior
TypeScript first - excellent type safety
Project Setup
Let's start by setting up our project with the necessary dependencies:
Package Dependencies
json
{
"name": "tanstacktable",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@faker-js/faker": "^8.0.2",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.9.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.0.1",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.1",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"vite": "^4.4.0"
}
}
Key Dependencies Explained:
@tanstack/react-table - The main table library
@faker-js/faker - Generates fake data for our demo
xlsx - Enables Excel file export
tailwindcss - For styling
Project Structure
src/
├── components/
│ ├── TanStackTable.jsx
│ ├── DebouncedInput.jsx
│ └── DownloadBtn.jsx
├── Icons/
│ └── Icons.jsx
├── data.js
├── App.jsx
├── main.jsx
└── index.css
Configuration Files
Tailwind Configuration
javascript
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
PostCSS Configuration
javascript
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Vite Configuration
javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
Styling Setup
Let's set up our global styles with Tailwind CSS:
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
.download-btn {
@apply bg-indigo-600 hover:bg-gray-100 hover:text-indigo-600 fill-white hover:fill-indigo-600 px-4 py-2
flex text-white items-center gap-2 rounded
}
Creating Icons
We'll create custom SVG icons for our search and download functionality:
src/Icons/Icons.jsx
export const SearchIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 512 512"
>
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" />
</svg>
)
}
export const DownloadIcon = () =>{
return <svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
className=""
viewBox="0 0 512 512"
>
<path d="M256 0a256 256 0 1 0 0 512A256 256 0 1 0 256 0zM376.9 294.6L269.8 394.5c-3.8 3.5-8.7 5.5-13.8 5.5s-10.1-2-13.8-5.5L135.1 294.6c-4.5-4.2-7.1-10.1-7.1-16.3c0-12.3 10-22.3 22.3-22.3l57.7 0 0-96c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 96 57.7 0c12.3 0 22.3 10 22.3 22.3c0 6.2-2.6 12.1-7.1 16.3z" />
</svg>
}
Generating Mock Data
For our demo, we'll use Faker.js to generate realistic user data:
src/data.js
// * fake data's
import { faker } from '@faker-js/faker';
export function createRandomUser() {
return {
profile: faker.image.avatar(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.datatype.number(40),
visits: faker.datatype.number(1000),
progress: faker.datatype.number(100),
};
}
export const USERS = faker.helpers.multiple(createRandomUser, {
count: 30,
});
Building the Debounced Input Component
To improve performance, we'll create a debounced search input that delays API calls:
src/components/DebouncedInput.jsx
import { useEffect, useState } from "react";
const DebouncedInput = ({
value: initValue,
onChange,
debounce = 500,
...props
}) => {
const [value, setValue] = useState(initValue);
useEffect(() => {
setValue(initValue);
}, [initValue]);
// * 0.5s after set value in state
useEffect(() => {
const timeout = setTimeout(() => {
onChange(value);
}, debounce);
return () => clearTimeout(timeout);
}, [value]);
return (
<input
{...props}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
};
export default DebouncedInput;
Key Features:
Debouncing - Waits 500ms before triggering the onChange callback
Performance - Reduces unnecessary re-renders and API calls
Flexibility - Customizable debounce delay
Creating the Download Button
Let's build a component that exports table data to Excel format:
src/components/DownloadBtn.jsx
import { DownloadIcon } from "../Icons/Icons";
import * as XLSX from "xlsx/xlsx.mjs";
const DownloadBtn = ({ data = [], fileName }) => {
return (
<button
className="download-btn"
onClick={() => {
const datas = data?.length ? data : [];
const worksheet = XLSX.utils.json_to_sheet(datas);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, fileName ? `${fileName}.xlsx` : "data.xlsx");
}}
>
<DownloadIcon />
Download
</button>
);
};
export default DownloadBtn;
Features:
Excel Export - Converts JSON data to Excel format
Custom Filename - Allows custom file naming
Error Handling - Handles empty data gracefully
The Main Table Component
Now for the centerpiece - our TanStack Table component:
src/components/TanStackTable.jsx
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import { USERS } from "../data";
import { useState } from "react";
import DownloadBtn from "./DownloadBtn";
import DebouncedInput from "./DebouncedInput";
import { SearchIcon } from "../Icons/Icons";
const TanStackTable = () => {
const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor("", {
id: "S.No",
cell: (info) => <span>{info.row.index + 1}</span>,
header: "S.No",
}),
columnHelper.accessor("profile", {
cell: (info) => (
<img
src={info?.getValue()}
alt="..."
className="rounded-full w-10 h-10 object-cover"
/>
),
header: "Profile",
}),
columnHelper.accessor("firstName", {
cell: (info) => <span>{info.getValue()}</span>,
header: "First Name",
}),
columnHelper.accessor("lastName", {
cell: (info) => <span>{info.getValue()}</span>,
header: "Last Name",
}),
columnHelper.accessor("age", {
cell: (info) => <span>{info.getValue()}</span>,
header: "Age",
}),
columnHelper.accessor("visits", {
cell: (info) => <span>{info.getValue()}</span>,
header: "Visits",
}),
columnHelper.accessor("progress", {
cell: (info) => <span>{info.getValue()}</span>,
header: "Progress",
}),
];
const [data] = useState(() => [...USERS]);
const [globalFilter, setGlobalFilter] = useState("");
const table = useReactTable({
data,
columns,
state: {
globalFilter,
},
getFilteredRowModel: getFilteredRowModel(),
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div className="p-2 max-w-5xl mx-auto text-white fill-gray-400">
<div className="flex justify-between mb-2">
<div className="w-full flex items-center gap-1">
<SearchIcon />
<DebouncedInput
value={globalFilter ?? ""}
onChange={(value) => setGlobalFilter(String(value))}
className="p-2 bg-transparent outline-none border-b-2 w-1/5 focus:w-1/3 duration-300 border-indigo-500"
placeholder="Search all columns..."
/>
</div>
<DownloadBtn data={data} fileName={"peoples"} />
</div>
<table className="border border-gray-700 w-full text-left">
<thead className="bg-indigo-600">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} className="capitalize px-3.5 py-2">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row, i) => (
<tr
key={row.id}
className={`
${i % 2 === 0 ? "bg-gray-900" : "bg-gray-800"}
`}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-3.5 py-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))
) : (
<tr className="text-center h-32">
<td colSpan={12}>No Record Found!</td>
</tr>
)}
</tbody>
</table>
{/* pagination */}
<div className="flex items-center justify-end mt-2 gap-2">
<button
onClick={() => {
table.previousPage();
}}
disabled={!table.getCanPreviousPage()}
className="p-1 border border-gray-300 px-2 disabled:opacity-30"
>
{"<"}
</button>
<button
onClick={() => {
table.nextPage();
}}
disabled={!table.getCanNextPage()}
className="p-1 border border-gray-300 px-2 disabled:opacity-30"
>
{">"}
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</strong>
</span>
<span className="flex items-center gap-1">
| Go to page:
<input
type="number"
defaultValue={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-16 bg-transparent"
/>
</span>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
className="p-2 bg-transparent"
>
{[10, 20, 30, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
);
};
export default TanStackTable;
App Component and Main Entry
src/App.jsx
import TanStackTable from "./components/TanStackTable";
const App = () => {
return (
<div className="pt-4 min-h-screen bg-gray-900">
<TanStackTable />
</div>
);
};
export default App;
src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
HTML Template
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TanStack Table Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Key Features Breakdown
1. Column Definition
const columns = [
columnHelper.accessor("", {
id: "S.No",
cell: (info) => <span>{info.row.index + 1}</span>,
header: "S.No",
}),
// ... more columns
];
2. Table Instance
const table = useReactTable({
data,
columns,
state: { globalFilter },
getFilteredRowModel: getFilteredRowModel(),
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
3. Global Search
<DebouncedInput
value={globalFilter ?? ""}
onChange={(value) => setGlobalFilter(String(value))}
className="p-2 bg-transparent outline-none border-b-2 w-1/5 focus:w-1/3 duration-300 border-indigo-500"
placeholder="Search all columns..."
/>
4. Pagination Controls
<div className="flex items-center justify-end mt-2 gap-2">
<button onClick={() => table.previousPage()}>{"<"}</button>
<button onClick={() => table.nextPage()}>{">"}</button>
<span>Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</span>
<select onChange={(e) => table.setPageSize(Number(e.target.value))}>
{[10, 20, 30, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>Show {pageSize}</option>
))}
</select>
</div>
Performance Optimizations
Debounced Search - Reduces unnecessary filtering operations
Memoized Data - Uses
useState
with initializer functionVirtual Scrolling - TanStack Table supports virtual scrolling for large datasets
Selective Re-renders - Only affected components re-render on state changes
Styling Highlights
Dark Theme Design
Background:
bg-gray-900
Table headers:
bg-indigo-600
Alternating row colors:
bg-gray-900
andbg-gray-800
Interactive Elements
Expanding search input:
focus:w-1/3 duration-300
Hover effects on buttons:
hover:bg-gray-100 hover:text-indigo-600
Disabled button states:
disabled:opacity-30
Running the Project
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
Extending the Table
Adding Sorting
import { getSortedRowModel } from "@tanstack/react-table";
// Add to table instance
getSortedRowModel: getSortedRowModel(),
// Add to column definition
columnHelper.accessor("firstName", {
header: ({ column }) => (
<button onClick={() => column.toggleSorting()}>
First Name {column.getIsSorted() === "asc" ? "↑" : column.getIsSorted() === "desc" ? "↓" : ""}
</button>
),
})
Adding Row Selection
const [rowSelection, setRowSelection] = useState({});
// Add to table instance
state: {
globalFilter,
rowSelection,
},
onRowSelectionChange: setRowSelection,
enableRowSelection: true,
Best Practices
Data Management - Keep data immutable and use proper state management
Performance - Implement virtual scrolling for large datasets
Accessibility - Add proper ARIA labels and keyboard navigation
Error Handling - Handle loading states and error conditions
TypeScript - Use TypeScript for better type safety
Testing - Write unit tests for table logic
Conclusion
We've successfully built a feature-rich data table using TanStack Table with React and Tailwind CSS. Our table includes:
✅ Global search functionality ✅ Pagination with customizable page sizes ✅ Excel export capability ✅ Responsive design ✅ Professional styling ✅ Performance optimizations
TanStack Table's headless approach gives us complete control over the UI while handling all the complex table logic. Combined with Tailwind CSS, we've created a modern, performant, and user-friendly data table that can be easily customized and extended.
The complete source code is available, and you can extend this foundation to add features like sorting, filtering, row selection, and more advanced functionality as your application grows.