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.
 




