Code A Program
Tanstack Table Design Using React js and tailwind css search, pagination, download option
Published on August 22, 2025

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

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

JavaScript
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

PostCSS Configuration

javascript

JavaScript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Vite Configuration

javascript

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

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

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

JavaScript
// * 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

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

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

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

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

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

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

JavaScript
const columns = [
  columnHelper.accessor("", {
    id: "S.No",
    cell: (info) => <span>{info.row.index + 1}</span>,
    header: "S.No",
  }),
  // ... more columns
];

2. Table Instance

JavaScript
const table = useReactTable({
  data,
  columns,
  state: { globalFilter },
  getFilteredRowModel: getFilteredRowModel(),
  getCoreRowModel: getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
});

3. Global Search

JavaScript
<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

JavaScript
<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

  1. Debounced Search - Reduces unnecessary filtering operations

  2. Memoized Data - Uses useState with initializer function

  3. Virtual Scrolling - TanStack Table supports virtual scrolling for large datasets

  4. 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 and bg-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

Bash
# Install dependencies
npm install

# Start development server
npm run dev

# Build for production
npm run build

Extending the Table

Adding Sorting

JavaScript
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

JavaScript
const [rowSelection, setRowSelection] = useState({});

// Add to table instance
state: {
  globalFilter,
  rowSelection,
},
onRowSelectionChange: setRowSelection,
enableRowSelection: true,

Best Practices

  1. Data Management - Keep data immutable and use proper state management

  2. Performance - Implement virtual scrolling for large datasets

  3. Accessibility - Add proper ARIA labels and keyboard navigation

  4. Error Handling - Handle loading states and error conditions

  5. TypeScript - Use TypeScript for better type safety

  6. 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 functionalityPagination with customizable page sizesExcel export capabilityResponsive designProfessional stylingPerformance 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.

Share:
Download Source Code

Get the complete source code for this tutorial. Includes all files, components, and documentation to help you follow along.

View on GitHub

📦What's Included:

  • • Complete source code files
  • • All assets and resources used
  • • README with setup instructions
  • • Package.json with dependencies
💡Free to use for personal and commercial projects