Powerful TanStack Tables

December 8, 2025

TanStack Table - Build powerful tables in ReactJs

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:

TanStack Table - Build powerful tables in ReactJs

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/