TanStack Table
HeadlessUI components for building highly interactive and accessible tables. In these example we have used our own Table components with TanStack table.
0 of 15 row(s) selected.
Installation
- Install the @tanstack/react-table package.
npm install @tanstack/react-table
- 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>
);
}
- 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>
),
}),
];
- 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",
},
];
- 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>
);
}
- 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>
);
}
}
- 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>
);
}
}
- 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
- Note: For more information, please refer to TanStack Table Documentation.