Skip to main content

TanStack Table

HeadlessUI components for building highly interactive and accessible tables. In these example we have used our own Table components with TanStack table.

CustomerDue DateAmountStatus
Francis Sanford MD

Francis Sanford MD

marya.barrow@yahoo.com

$ 544

Paid

Lucia Kshlerin

Lucia Kshlerin

mason_davis4@yahoo.com

$ 560

Pending

Byron Hoppe III

Byron Hoppe III

jayda_schill35@yahoo.com

$ 249

Pending

Camille Jenkins

Camille Jenkins

retha.lehne47@hotmail.com

$ 255

Draft

Kelli Mitchell

Kelli Mitchell

guise.champ@hotmail.com

$ 329

Paid

Installation


  1. Install the @tanstack/react-table package.
npm install @tanstack/react-table
  1. Create a table component table.tsx
import React from "react";
import { Table } from "rizzui";
import { type Person } from "./data";
import { flexRender, Table as TanStackTableTypes } from "@tanstack/react-table";

type TablePropsTypes = {
table: TanStackTableTypes<Person>;
};

export default function MainTable({ table }: TablePropsTypes) {
const footers = table
.getFooterGroups()
.map((group) => group.headers.map((header) => header.column.columnDef.footer))
.flat()
.filter(Boolean);

return (
<div className="w-full overflow-x-auto overflow-y-hidden custom-scrollbar">
<Table
className="!shadow-none !border-0"
style={{
width: table.getTotalSize(),
}}
>
<Table.Header className="!bg-gray-100 !border-y-0">
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<Table.Head
key={header.id}
colSpan={header.colSpan}
style={{
width: header.getSize(),
}}
className="!text-start"
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</Table.Head>
);
})}
</Table.Row>
);
})}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.map((row) => (
<Table.Row key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.Cell
key={cell.id}
className="!text-start"
style={{
width: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>

{footers.length > 0 && (
<Table.Footer>
{table.getFooterGroups().map((footerGroup) => (
<Table.Row key={footerGroup.id}>
{footerGroup.headers.map((footer) => {
return (
<Table.Cell key={footer.id}>
{footer.isPlaceholder ? null : (
<>{flexRender(footer.column.columnDef.footer, footer.getContext())}</>
)}
</Table.Cell>
);
})}
</Table.Row>
))}
</Table.Footer>
)}
</Table>
</div>
);
}
  1. Create a column.tsx file
import { type Person } from "./data";
import { ActionIcon, Badge, Button, Checkbox, Popover, Text } from "rizzui";
import { createColumnHelper } from "@tanstack/react-table";
import { AvatarCard, DateCell, getStatusBadge } from "./utils";
import {
EllipsisHorizontalIcon,
EyeIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";

const columnHelper = createColumnHelper<Person>();

export const defaultColumns = [
columnHelper.accessor("id", {
size: 50,
header: ({ table }) => (
<Checkbox
className="ps-2"
inputClassName="bg-white"
aria-label="Select all rows"
checked={table.getIsAllPageRowsSelected()}
onChange={() => table.toggleAllPageRowsSelected()}
/>
),
cell: ({ row }) => (
<Checkbox
className="ps-2"
aria-label="Select row"
checked={row.getIsSelected()}
onChange={() => row.toggleSelected()}
/>
),
}),

columnHelper.accessor("name", {
size: 280,
header: "Customer",
cell: ({ row: { original } }) => (
<AvatarCard
src={original.avatar}
name={original.name}
description={original.email.toLowerCase()}
/>
),
}),

columnHelper.accessor("dueDate", {
size: 180,
header: "Due Date",
cell: ({ row }) => <DateCell date={new Date(row.original.dueDate)} />,
}),

columnHelper.accessor("amount", {
size: 120,
header: "Amount",
cell: ({ row }) => <span className="font-medium">$ {row.original.amount}</span>,
}),

columnHelper.accessor("status", {
size: 120,
header: "Status",
cell: (info) => getStatusBadge(info.renderValue()!),
}),

columnHelper.accessor("avatar", {
size: 120,
header: "",
cell: () => (
<div className="w-full flex justify-center">
<Popover
shadow="sm"
placement="bottom-end"
>
<Popover.Trigger>
<ActionIcon variant="text">
<EllipsisHorizontalIcon
strokeWidth={2}
className="size-5"
/>
</ActionIcon>
</Popover.Trigger>
<Popover.Content className="max-w-40 grid grid-cols-1 gap-1 p-1">
<Button
variant="text"
className="hover:bg-gray-100 gap-2"
>
<PencilIcon className="size-4" /> Edit
</Button>
<Button
variant="text"
className="hover:bg-gray-100 gap-2"
>
<EyeIcon className="size-4" /> View
</Button>
<Button
variant="text"
color="danger"
className="hover:bg-gray-100 gap-2"
>
<TrashIcon className="size-4" /> Delete
</Button>
</Popover.Content>
</Popover>
</div>
),
}),
];

  1. Create a data.ts file
export type Person = {
id: string;
name: string;
userName: string;
avatar: string;
email: string;
dueDate: string;
amount: number;
status: string;
};

export const defaultData = [
{
id: "62447",
name: "Francis Sanford MD",
userName: "George33",
avatar: "https://randomuser.me/api/portraits/women/8.jpg",
email: "Marya.Barrow@yahoo.com",
dueDate: "2023-10-18T13:24:00.760Z",
amount: 544,
status: "Paid",
},
{
id: "86740",
name: "Lucia Kshlerin",
userName: "Kenyon_Goldner56",
avatar: "https://randomuser.me/api/portraits/women/2.jpg",
email: "Mason_Davis4@yahoo.com",
dueDate: "2023-07-18T01:06:16.095Z",
amount: 560,
status: "Pending",
},
{
id: "42548",
name: "Byron Hoppe III",
userName: "Walton.Hane98",
avatar: "https://randomuser.me/api/portraits/men/75.jpg",
email: "Jayda_Schill35@yahoo.com",
dueDate: "2024-12-18T15:32:21.317Z",
amount: 249,
status: "Pending",
},
{
id: "97024",
name: "Camille Jenkins",
userName: "Dalton_Von55",
avatar: "https://randomuser.me/api/portraits/men/9.jpg",
email: "Retha.Lehne47@hotmail.com",
dueDate: "2024-06-30T19:06:03.018Z",
amount: 255,
status: "Draft",
},
{
id: "14608",
name: "Kelli Mitchell",
userName: "Iva.Denesik",
avatar: "https://randomuser.me/api/portraits/men/22.jpg",
email: "Guise.Champ@hotmail.com",
dueDate: "2025-07-24T18:45:02.179Z",
amount: 329,
status: "Paid",
},
{
id: "95656",
name: "Randall Kuhic",
userName: "Henry_Quigley0",
avatar: "https://randomuser.me/api/portraits/men/1.jpg",
email: "Simeon93@yahoo.com",
dueDate: "2023-11-02T00:20:47.253Z",
amount: 402,
status: "Paid",
},
{
id: "73151",
name: "Jody Carroll",
userName: "Lavon32",
avatar: "https://randomuser.me/api/portraits/women/8.jpg",
email: "Frieda_Renne@gmail.com",
dueDate: "2024-01-03T02:53:29.596Z",
amount: 977,
status: "Overdue",
},
{
id: "57931",
name: "Jill Russel",
userName: "Abdiel.Terry",
avatar: "https://randomuser.me/api/portraits/women/2.jpg",
email: "Cleora.Murra@hotmail.com",
dueDate: "2025-01-23T08:52:39.081Z",
amount: 736,
status: "Paid",
},
{
id: "36515",
name: "Genevieve Hammes",
userName: "Kian_Huels",
avatar: "https://randomuser.me/api/portraits/men/75.jpg",
email: "Bernard63@yahoo.com",
dueDate: "2024-07-29T18:18:19.193Z",
amount: 755,
status: "Draft",
},
{
id: "34893",
name: "Alejandro Reichert",
userName: "Timothy91",
avatar: "https://randomuser.me/api/portraits/men/9.jpg",
email: "Wava.Mulle47@gmail.com",
dueDate: "2023-05-04T04:33:47.908Z",
amount: 240,
status: "Draft",
},
{
id: "66356",
name: "Ricardo Kling",
userName: "Celia.Shanahan86",
avatar: "https://randomuser.me/api/portraits/men/22.jpg",
email: "Gene73@yahoo.com",
dueDate: "2025-04-16T11:49:15.276Z",
amount: 852,
status: "Overdue",
},
{
id: "81467",
name: "Carl Bode",
userName: "Pablo_Thompson",
avatar: "https://randomuser.me/api/portraits/men/1.jpg",
email: "Virgil.Skile@hotmail.com",
dueDate: "2024-05-28T04:44:49.629Z",
amount: 295,
status: "Draft",
},
{
id: "14042",
name: "Sherry Weber",
userName: "Shane39",
avatar: "https://randomuser.me/api/portraits/women/8.jpg",
email: "Aidan22@hotmail.com",
dueDate: "2025-11-30T00:34:34.822Z",
amount: 318,
status: "Paid",
},
{
id: "49825",
name: "Erika O'Reilly",
userName: "Hazle_Bednar95",
avatar: "https://randomuser.me/api/portraits/women/2.jpg",
email: "Ardith57@yahoo.com",
dueDate: "2024-05-17T06:24:33.253Z",
amount: 463,
status: "Overdue",
},
{
id: "81929",
name: "Lillian Anderson",
userName: "Cyrus_Hettinger",
avatar: "https://randomuser.me/api/portraits/men/75.jpg",
email: "Aletha_Watrs87@gmail.com",
dueDate: "2023-12-29T04:41:54.007Z",
amount: 196,
status: "Pending",
},
{
id: "25086",
name: "Connie Braun",
userName: "Ramona99",
avatar: "https://randomuser.me/api/portraits/men/9.jpg",
email: "Mervin.Ruerford@hotmail.com",
dueDate: "2024-12-27T21:39:17.142Z",
amount: 384,
status: "Draft",
},
{
id: "79737",
name: "Mattie Miller",
userName: "Madison_MacGyver-Lesch52",
avatar: "https://randomuser.me/api/portraits/men/22.jpg",
email: "Bianka30@yahoo.com",
dueDate: "2025-06-27T15:53:33.802Z",
amount: 812,
status: "Paid",
},
{
id: "66747",
name: "Shelley Lind-VonRueden",
userName: "Issac_West",
avatar: "https://randomuser.me/api/portraits/men/1.jpg",
email: "Destini_Wiamson34@yahoo.com",
dueDate: "2024-12-29T09:04:48.858Z",
amount: 596,
status: "Pending",
},
{
id: "44937",
name: "Manuel Langworth",
userName: "Kelley71",
avatar: "https://randomuser.me/api/portraits/men/75.jpg",
email: "Philip.OKfe94@gmail.com",
dueDate: "2025-12-20T09:41:31.402Z",
amount: 545,
status: "Pending",
},
{
id: "11420",
name: "Dr. Guillermo Huels Jr.",
userName: "Linnie.Hane",
avatar: "https://randomuser.me/api/portraits/women/8.jpg",
email: "Ricky41@yahoo.com",
dueDate: "2023-03-26T14:06:10.093Z",
amount: 537,
status: "Paid",
},
];
  1. Create a pagination.tsx file
import { ActionIcon, Select, SelectOption, Text } from "rizzui";
import { type Table as ReactTableType } from "@tanstack/react-table";
import {
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "@heroicons/react/20/solid";

const options = [
{ value: 5, label: "5" },
{ value: 10, label: "10" },
{ value: 15, label: "15" },
{ value: 20, label: "20" },
];

export default function TablePagination<TData extends Record<string, any>>({
table,
}: {
table: ReactTableType<TData>;
}) {
return (
<div className="flex w-full items-center justify-between @container">
<div className="hidden @2xl:block">
<Text>
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</Text>
</div>
<div className="flex w-full items-center justify-between gap-6 @2xl:w-auto @2xl:gap-12">
<div className="flex items-center gap-4">
<Text className="hidden font-medium text-gray-900 @md:block">Rows per page</Text>
<Select
options={options}
className="w-[70px]"
value={table.getState().pagination.pageSize}
onChange={(v: SelectOption) => {
table.setPageSize(Number(v.value));
}}
selectClassName="font-semibold text-sm ring-0 shadow-sm h-9"
optionClassName="justify-center font-medium"
/>
</div>
<Text className="hidden font-medium text-gray-900 @3xl:block">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount().toLocaleString()}
</Text>
<div className="grid grid-cols-4 gap-2">
<ActionIcon
rounded="lg"
variant="outline"
aria-label="Go to first page"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
className="text-gray-900 shadow-sm disabled:text-gray-400 disabled:shadow-none"
>
<ChevronDoubleLeftIcon className="size-5" />
</ActionIcon>
<ActionIcon
rounded="lg"
variant="outline"
aria-label="Go to previous page"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="text-gray-900 shadow-sm disabled:text-gray-400 disabled:shadow-none"
>
<ChevronLeftIcon className="size-5" />
</ActionIcon>
<ActionIcon
rounded="lg"
variant="outline"
aria-label="Go to next page"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="text-gray-900 shadow-sm disabled:text-gray-400 disabled:shadow-none"
>
<ChevronRightIcon className="size-5" />
</ActionIcon>
<ActionIcon
rounded="lg"
variant="outline"
aria-label="Go to last page"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
className="text-gray-900 shadow-sm disabled:text-gray-400 disabled:shadow-none"
>
<ChevronDoubleRightIcon className="size-5" />
</ActionIcon>
</div>
</div>
</div>
);
}
  1. Create a toolbar.tsx file
import { ActionIcon, Badge, Button, Checkbox, Input, Popover, Select, Text, Title } from "rizzui";
import { type Table as ReactTableType } from "@tanstack/react-table";
import {
AdjustmentsHorizontalIcon,
MagnifyingGlassIcon,
TrashIcon,
} from "@heroicons/react/20/solid";

interface TableToolbarProps<T extends Record<string, any>> {
table: ReactTableType<T>;
}

const statusOptions = [
{ label: "Paid", value: "Paid" },
{ label: "Pending", value: "Pending" },
{ label: "Draft", value: "Draft" },
];

export default function TableToolbar<TData extends Record<string, any>>({
table,
}: TableToolbarProps<TData>) {
const isFiltered = table.getState().globalFilter || table.getState().columnFilters.length > 0;

return (
<div className="flex items-center justify-between w-full mb-4">
<Input
type="search"
placeholder="Search by anything..."
value={table.getState().globalFilter ?? ""}
onClear={() => table.setGlobalFilter("")}
onChange={(e) => table.setGlobalFilter(e.target.value)}
inputClassName="h-9"
clearable={true}
prefix={<MagnifyingGlassIcon className="size-4" />}
/>
<div className="flex items-center gap-4">
<Select
options={statusOptions}
value={table.getColumn("status")?.getFilterValue() ?? []}
onChange={(e) => table.getColumn("status")?.setFilterValue(e)}
getOptionValue={(option: { value: any }) => option.value}
getOptionDisplayValue={(option: { value: string }) =>
renderOptionDisplayValue(option.value)
}
placeholder="Status..."
displayValue={(selected: string) => renderOptionDisplayValue(selected)}
className={"w-32"}
dropdownClassName="!z-20 h-auto"
selectClassName="h-[38px] ring-0"
/>

{isFiltered && (
<Button
onClick={() => {
table.resetGlobalFilter();
table.resetColumnFilters();
}}
variant="flat"
className="gap-2"
>
<TrashIcon className="size-4" /> Clear
</Button>
)}

{table && (
<Popover
shadow="sm"
placement="bottom-end"
>
<Popover.Trigger>
<ActionIcon title={"Toggle Columns"}>
<AdjustmentsHorizontalIcon className="size-[18px]" />
</ActionIcon>
</Popover.Trigger>
<Popover.Content className="z-0">
<>
<Title
as="h6"
className="!mb-4 text-sm font-semibold"
>
Toggle Columns
</Title>
<div className="grid grid-cols-1 gap-4">
{table.getAllLeafColumns().map((column) => {
return (
typeof column.columnDef.header === "string" &&
column.columnDef.header.length > 0 && (
<Checkbox
size="sm"
key={column.id}
label={<>{column.columnDef.header}</>}
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
iconClassName="size-4 translate-x-0.5"
/>
)
);
})}
</div>
</>
</Popover.Content>
</Popover>
)}
</div>
</div>
);
}

export function renderOptionDisplayValue(value: string) {
switch (value.toLowerCase()) {
case "pending":
return (
<div className="flex items-center">
<Badge
color="warning"
renderAsDot
/>
<Text className="ms-2 font-medium capitalize text-orange-dark">{value}</Text>
</div>
);
case "paid":
return (
<div className="flex items-center">
<Badge
color="success"
renderAsDot
/>
<Text className="ms-2 font-medium capitalize text-green-dark">{value}</Text>
</div>
);
case "overdue":
return (
<div className="flex items-center">
<Badge
color="danger"
renderAsDot
/>
<Text className="ms-2 font-medium capitalize text-red-dark">{value}</Text>
</div>
);
default:
return (
<div className="flex items-center">
<Badge
renderAsDot
className="bg-gray-400"
/>
<Text className="ms-2 font-medium capitalize text-gray-600">{value}</Text>
</div>
);
}
}
  1. Create a utils.tsx file
  • Note: This file is not required. If you don't need the insider components, you can remove this file. And all the components used in other file.
import dayjs from "dayjs";
import { Avatar, AvatarProps, Badge, cn, Text } from "rizzui";

interface AvatarCardProps {
src: string;
name: string;
className?: string;
description?: string;
avatarProps?: AvatarProps;
}

export function AvatarCard({ src, name, className, description, avatarProps }: AvatarCardProps) {
return (
<figure className={cn("flex items-center gap-3", className)}>
<Avatar
name={name}
src={src}
{...avatarProps}
/>
<figcaption className="grid gap-0.5">
<Text className="font-lexend text-sm font-medium text-gray-900 dark:text-gray-700">
{name}
</Text>
{description && <Text className="text-[13px] text-gray-500">{description}</Text>}
</figcaption>
</figure>
);
}

interface DateCellProps {
date: Date;
className?: string;
dateFormat?: string;
dateClassName?: string;
timeFormat?: string;
timeClassName?: string;
}

export function DateCell({
date,
className,
timeClassName,
dateClassName,
dateFormat = "MMMM D, YYYY",
timeFormat = "h:mm A",
}: DateCellProps) {
return (
<div className={cn("grid gap-1", className)}>
<time
dateTime={formatDate(date, "YYYY-MM-DD")}
className={cn("font-medium text-gray-700", dateClassName)}
>
{formatDate(date, dateFormat)}
</time>
<time
dateTime={formatDate(date, "HH:mm:ss")}
className={cn("text-[13px] text-gray-500", timeClassName)}
>
{formatDate(date, timeFormat)}
</time>
</div>
);
}

export function formatDate(date?: Date, format: string = "DD MMM, YYYY"): string {
if (!date) return "";
return dayjs(date).format(format);
}

export function getStatusBadge(status: string) {
switch (status?.toLowerCase()) {
case "pending":
return (
<div className="flex items-center">
<Badge
color="warning"
renderAsDot
/>
<Text className="ms-2 font-medium text-orange-dark">{status}</Text>
</div>
);
case "paid":
return (
<div className="flex items-center">
<Badge
color="success"
renderAsDot
/>
<Text className="ms-2 font-medium text-green-dark">{status}</Text>
</div>
);
case "overdue":
return (
<div className="flex items-center">
<Badge
color="danger"
renderAsDot
/>
<Text className="ms-2 font-medium text-red-dark">{status}</Text>
</div>
);
default:
return (
<div className="flex items-center">
<Badge
renderAsDot
className="bg-gray-400"
/>
<Text className="ms-2 font-medium text-gray-600">{status}</Text>
</div>
);
}
}
  1. In your page file use the MainTable component.
import MainTable from "./table";
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import { defaultData } from "./data";
import { defaultColumns } from "./column";
import TableToolbar from "./toolbar";
import TablePagination from "./pagination";

export default function TanStackTableDemo() {
const table = useReactTable({
data: defaultData,
columns: defaultColumns,
initialState: {
pagination: {
pageIndex: 0,
pageSize: 5,
},
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});

return (
<>
<TableToolbar table={table} />
<MainTable table={table} />
<TablePagination table={table} />
</>
);
}

API Reference