Skip to main content

TanStack Table

A powerful, headless table component built on top of TanStack Table (formerly React Table). Seamlessly integrated with RizzUI's design system, this component provides a flexible and performant solution for building complex, interactive data tables with sorting, filtering, pagination, and row selection.

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

Features

  • 🎯 Headless Architecture - Full control over table rendering and styling
  • 🔍 Advanced Filtering - Global search and column-specific filters
  • 📊 Sorting & Pagination - Built-in sorting and pagination capabilities
  • Row Selection - Single and multi-row selection with checkboxes
  • 🎨 Theme Integration - Fully integrated with RizzUI theme colors and design tokens
  • 🌓 Dark Mode Support - Automatic dark mode styling using RizzUI theme variables
  • 📱 Responsive - Works seamlessly across all device sizes
  • Performant - Optimized for large datasets with virtual scrolling support
  • 🎛️ Column Visibility - Toggle column visibility dynamically
  • Accessible - Built with accessibility best practices

Installation


Before using the TanStack Table component, you'll need to install the required dependency:

Step 1

Install the @tanstack/react-table package.

npm install @tanstack/react-table

Step 2

Create a table component, table.tsx

import React from 'react';
import { Table } from 'rizzui/table';
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-[var(--muted)] !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>
);
}

Step 3

Create a column.tsx file to define your table columns.

import { type Person } from './data';
import { ActionIcon } from 'rizzui/action-icon';
import { Button } from 'rizzui/button';
import { Checkbox } from 'rizzui/checkbox';
import { Popover } from 'rizzui/popover';
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()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={() => table.toggleAllPageRowsSelected()}
/>
),
cell: ({ row }) => (
<Checkbox
className="ps-2"
aria-label="Select row"
checked={row.getIsSelected()}
indeterminate={row.getIsSomeSelected()}
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 as="span" variant="text" className="h-auto p-0">
<EllipsisHorizontalIcon strokeWidth={2} className="size-5" />
</ActionIcon>
</Popover.Trigger>
<Popover.Content className="p-1 flex flex-col">
<Button
size="sm"
variant="text"
className="hover:bg-[var(--muted)] gap-2 justify-start"
>
<PencilIcon className="size-3.5" /> Edit
</Button>
<Button
size="sm"
variant="text"
className="hover:bg-[var(--muted)] gap-2 justify-start"
>
<EyeIcon className="size-3.5" /> View
</Button>
<Button
size="sm"
variant="text"
className="hover:bg-[var(--muted)] gap-2 justify-start"
>
<TrashIcon className="size-3.5" /> Delete
</Button>
</Popover.Content>
</Popover>
</div>
),
}),
];

Step 4

Create a data.ts file to define your data structure and sample data.

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',
},
// ... more data
];

Step 5

Create a pagination.tsx file for table pagination controls.

import { ActionIcon } from 'rizzui/action-icon';
import { Select, SelectOption } from 'rizzui/select';
import { Text } from 'rizzui/typography';
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 text-sm font-medium text-[var(--text-secondary)] @md:block">
Rows per page
</Text>
<Select
size="sm"
options={options}
className="w-[52px]"
value={table.getState().pagination.pageSize}
onChange={(v: SelectOption) => {
table.setPageSize(Number(v.value));
}}
selectClassName="font-semibold text-sm ring-0 shadow-sm"
optionClassName="justify-center font-medium"
suffixClassName="[&_svg]:!size-3"
/>
</div>
<Text className="hidden text-sm font-medium text-[var(--text-secondary)] @3xl:block">
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount().toLocaleString()}
</Text>
<div className="grid grid-cols-4 gap-2">
<ActionIcon
size="sm"
rounded="lg"
variant="outline"
aria-label="Go to first page"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
className="text-[var(--text-primary)] shadow-sm disabled:text-[var(--muted-foreground)] disabled:shadow-none"
>
<ChevronDoubleLeftIcon className="size-5" />
</ActionIcon>
<ActionIcon
size="sm"
rounded="lg"
variant="outline"
aria-label="Go to previous page"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="text-[var(--text-primary)] shadow-sm disabled:text-[var(--muted-foreground)] disabled:shadow-none"
>
<ChevronLeftIcon className="size-5" />
</ActionIcon>
<ActionIcon
size="sm"
rounded="lg"
variant="outline"
aria-label="Go to next page"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="text-[var(--text-primary)] shadow-sm disabled:text-[var(--muted-foreground)] disabled:shadow-none"
>
<ChevronRightIcon className="size-5" />
</ActionIcon>
<ActionIcon
size="sm"
rounded="lg"
variant="outline"
aria-label="Go to last page"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
className="text-[var(--text-primary)] shadow-sm disabled:text-[var(--muted-foreground)] disabled:shadow-none"
>
<ChevronDoubleRightIcon className="size-5" />
</ActionIcon>
</div>
</div>
</div>
);
}

Step 6

Create a toolbar.tsx file for table toolbar with search and filters.

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="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 gap-2">
<Badge color="warning" renderAsDot />
<Text className="font-medium capitalize text-orange-dark">
{value}
</Text>
</div>
);
case 'paid':
return (
<div className="flex items-center gap-2">
<Badge color="success" renderAsDot />
<Text className="font-medium capitalize text-green-dark">
{value}
</Text>
</div>
);
case 'overdue':
return (
<div className="flex items-center gap-2">
<Badge color="danger" renderAsDot />
<Text className="font-medium capitalize text-red-dark">{value}</Text>
</div>
);
default:
return (
<div className="flex items-center gap-2">
<Badge renderAsDot className="bg-[var(--muted-foreground)]" />
<Text className="font-medium capitalize text-[var(--text-secondary)]">
{value}
</Text>
</div>
);
}
}

Step 7

Create a utils.tsx file for utility components (optional).

import dayjs from 'dayjs';
import { Avatar, AvatarProps } from 'rizzui/avatar';
import { Badge } from 'rizzui/badge';
import { Text } from 'rizzui/typography';
import { cn } 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-[var(--text-primary)] dark:text-[var(--text-secondary)]">
{name}
</Text>
{description && (
<Text className="!text-[13px] !leading-normal text-[var(--muted-foreground)]">
{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-0', className)}>
<time
dateTime={formatDate(date, 'YYYY-MM-DD')}
className={cn(
'font-medium text-[var(--text-secondary)]',
dateClassName
)}
>
{formatDate(date, dateFormat)}
</time>
<time
dateTime={formatDate(date, 'HH:mm:ss')}
className={cn(
'text-[13px] text-[var(--muted-foreground)] leading-normal',
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 gap-2">
<Badge color="warning" renderAsDot />
<Text className="font-medium text-orange-dark">{status}</Text>
</div>
);
case 'paid':
return (
<div className="flex items-center gap-2">
<Badge color="success" renderAsDot />
<Text className="font-medium text-green-dark">{status}</Text>
</div>
);
case 'overdue':
return (
<div className="flex items-center gap-2">
<Badge color="danger" renderAsDot />
<Text className="font-medium text-red-dark">{status}</Text>
</div>
);
default:
return (
<div className="flex items-center gap-2">
<Badge renderAsDot className="bg-[var(--muted-foreground)]" />
<Text className="font-medium text-[var(--text-secondary)]">
{status}
</Text>
</div>
);
}
}

Step 8

Use the components in your page file.
import React from 'react';
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 [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data: defaultData,
columns: defaultColumns,
initialState: {
pagination: {
pageIndex: 0,
pageSize: 5,
},
},
state: {
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});

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

Advanced Usage

Custom Column Rendering

Customize how columns are rendered:

columnHelper.accessor('amount', {
size: 120,
header: 'Amount',
cell: ({ row }) => {
const amount = row.original.amount;
return (
<span
className={cn(
'font-medium',
amount > 500 ? 'text-green-dark' : 'text-orange-dark'
)}
>
$ {amount}
</span>
);
},
});

Column Sorting

Enable sorting on columns:

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

Custom Filters

Add custom filtering logic:

const table = useReactTable({
// ... other config
getFilteredRowModel: getFilteredRowModel(),
filterFns: {
customFilter: (row, columnId, filterValue) => {
// Custom filter logic
return row.getValue(columnId) === filterValue;
},
},
});

Row Expansion

Add expandable rows:

columnHelper.display({
id: 'expander',
cell: ({ row }) => (
<button onClick={() => row.toggleExpanded()}>
{row.getIsExpanded() ? '▼' : '▶'}
</button>
),
});

Best Practices

  • Use memoization - Memoize columns and data to prevent unnecessary re-renders
  • Virtual scrolling - For large datasets, consider using virtual scrolling
  • Column sizing - Set appropriate column sizes for better layout control
  • Accessibility - Ensure proper ARIA labels and keyboard navigation
  • Performance - Use getFilteredRowModel and getPaginationRowModel for better performance
  • State management - Manage table state externally for better control
  • Type safety - Use TypeScript for type-safe column definitions
  • Theme consistency - Use RizzUI theme colors for consistent styling

API Reference

The TanStack Table component extends all features from TanStack Table. Refer to their documentation for a complete list of available APIs and options.

Common Table Options

OptionTypeDescription
dataTData[]Array of data to display in the table
columnsColumnDef<TData>[]Column definitions
getCoreRowModel() => RowModelCore row model getter
getFilteredRowModel() => RowModelFiltered row model getter
getPaginationRowModel() => RowModelPagination row model getter
enableRowSelectionbooleanEnable row selection
onRowSelectionChange(updater) => voidRow selection change handler
initialStateTableStateInitial table state

Note: For more information, please refer to the TanStack Table Documentation.