npm install @thrivecart/uiyarn add @thrivecart/uipnpm add @thrivecart/uibun add @thrivecart/uiimport { DataTable } from '@thrivecart/ui';
import type { ColumnDef } from '@tanstack/react-table';DataTable requires two props: data (array of row objects) and columns (TanStack Table column definitions). Everything else is optional.
Minimal table with columns and data
Showing 10 of 0 Item
import type { ColumnDef } from '@tanstack/react-table';
interface User {
id: string;
name: string;
email: string;
role: string;
}
const columns: ColumnDef<User>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'role', header: 'Role' },
];
const data: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'Editor' },
];
<DataTable data={data} columns={columns} />In production, table cells often contain rich content. Use the cell property in column definitions to render avatars, badges, links, and inline actions.
Columns with avatar, badge status, and link rendering
Showing 10 of 0 Item
const columns: ColumnDef<Contact>[] = [
{
accessorKey: 'name',
header: 'User',
cell: ({ row }) => (
<div className="flex items-center gap-3">
<Avatar size="sm">
<AvatarFallback>
{row.original.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{row.original.name}</div>
<div className="text-sm text-ink-light">{row.original.email}</div>
</div>
</div>
),
size: 400,
enableHiding: false,
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge variant={row.original.status === 'active' ? 'success' : 'default'}>
{row.original.status}
</Badge>
),
},
{
accessorKey: 'list_name',
header: 'List',
cell: ({ row }) => (
<span className="text-ink-light">{row.original.list_name || '—'}</span>
),
},
];Use TableCellActions to add hover-revealed action buttons to each row. Pass the cell's visible content via the content prop — actions overlay it on row hover. No extra column needed.
Hover-revealed action buttons overlaying the last cell
Showing 10 of 0 Item
import { TableCellActions, ButtonGroup, Button, IconButton, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, Badge } from '@thrivecart/ui';
import { MdMoreHoriz, MdOutlineEdit, MdOutlineEmail, MdOutlineDelete, MdOutlinePersonAdd, MdPersonOutline } from 'react-icons/md';
const columns = [
{ accessorKey: 'email', header: 'Contact', size: 300 },
{ accessorKey: 'role', header: 'Role' },
{
accessorKey: 'status',
header: 'Status',
cell: (info) => (
<TableCellActions
row={info.row}
table={info.table}
content={
<Badge variant={info.row.original.status === 'active' ? 'success' : 'default'}>
{info.row.original.status}
</Badge>
}
>
<ButtonGroup attached={true} size="sm">
<Button variant="secondary">View</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton variant="secondary" size="sm" aria-label="More options" icon={<MdMoreHoriz />} />
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-52">
<DropdownMenuItem><MdPersonOutline /> View Profile</DropdownMenuItem>
<DropdownMenuItem><MdOutlineEdit /> Edit Contact</DropdownMenuItem>
<DropdownMenuItem><MdOutlineEmail /> Send Email</DropdownMenuItem>
<DropdownMenuItem><MdOutlinePersonAdd /> Add to List</DropdownMenuItem>
<DropdownMenuItem variant="destructive"><MdOutlineDelete /> Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</TableCellActions>
),
},
];When using columnPinning, the primary column stays fixed while other columns scroll. This is useful for wide tables with many columns.
Pinned contact column stays visible while other columns scroll
Showing 10 of 0 Item
<DataTable
data={contacts}
columns={columns}
columnPinning={{ left: ['email'] }}
/>DataTable supports multiple filter types: select, multiselect, date, and boolean. Filters appear in the table toolbar and update tableState.filters.
Combine multiselect, date range, and other filter types
Showing 10 of 0 Item
<DataTable
data={contacts}
columns={columns}
filters={[
{
id: 'lists',
label: 'List(s)',
type: 'multiselect',
placeholder: 'Select lists',
searchPlaceholder: 'Search lists...',
searchable: true,
options: lists.map(list => ({ label: list.name, value: list.id })),
},
{
id: 'audiences',
label: 'Audiences',
type: 'multiselect',
placeholder: 'Select audiences',
searchPlaceholder: 'Search audiences...',
searchable: true,
options: audiences.map(a => ({ label: a.name, value: a.id })),
},
{
id: 'dateRange',
label: 'Sign Up Date',
type: 'date',
placeholder: 'Select date range',
mode: 'range',
},
{
id: 'status',
label: 'Status',
type: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
],
},
]}
/>Enable row selection to show a floating action bar. The actionBarActions prop receives the selected rows and renders bulk action buttons.
Select rows and perform bulk operations like send email, add to list, or delete
Showing 10 of 0 user
<DataTable
data={contacts}
columns={columns}
dataType="contact"
actionBarActions={(selectedContacts) => (
<ButtonGroup attached={false} size="sm">
<Button
variant="secondary"
size="sm"
leftIcon={<MdEmail />}
onClick={() => openDialog('sendEmail', { contacts: selectedContacts })}
>
Send Email
</Button>
<Button
variant="secondary"
size="sm"
leftIcon={<MdPersonAdd />}
onClick={() => openDialog('addToList', { contacts: selectedContacts })}
>
Add to List
</Button>
<Button
variant="destructive"
size="sm"
leftIcon={<MdDelete />}
onClick={() => openDialog('deleteContact', {
dialogTitle: `Delete ${selectedContacts.length} contact${selectedContacts.length === 1 ? '' : 's'}?`,
dialogDescription: 'This action cannot be undone.',
contacts: selectedContacts,
})}
>
Delete
</Button>
<IconButton variant="secondary" size="sm" icon={<MdMoreHoriz />} />
</ButtonGroup>
)}
/>Control which columns are visible with columnVisibility and onColumnVisibilityChange. Users can toggle columns via the built-in columns menu.
Let users show/hide columns dynamically
Showing 10 of 0 Item
function ContactsTable() {
const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({
phone: true,
list_name: true,
created_at: false, // hidden by default
});
return (
<DataTable
data={contacts}
columns={columns}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
);
}For server-side pagination, sorting, and filtering, use the controlled tableState and setTableState props. The table communicates all state changes through setTableState, and you send those to your API.
Server-side pagination, search, and filters using fetch and useEffect
Showing 10 of 0 Item
import { useState, useEffect, useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@thrivecart/ui';
import type { TableState } from '@thrivecart/ui';
interface Contact {
id: string;
name: string;
email: string;
status: string;
}
function ContactsPage() {
const [data, setData] = useState<Contact[]>([]);
const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [tableState, setTableState] = useState<TableState>({
pagination: { pageIndex: 0, pageSize: 10 },
sorting: [],
filters: {},
columns: [],
search: '',
view: 'list',
});
// Fetch data whenever tableState changes
useEffect(() => {
const fetchContacts = async () => {
setIsLoading(true);
try {
const params = new URLSearchParams({
page: String(tableState.pagination.pageIndex),
pageSize: String(tableState.pagination.pageSize),
search: tableState.search || '',
...Object.fromEntries(
Object.entries(tableState.filters).map(([k, v]) => [k, String(v)])
),
});
const res = await fetch(`/api/contacts?${params}`);
const json = await res.json();
setData(json.data);
setTotal(json.total);
} finally {
setIsLoading(false);
}
};
fetchContacts();
}, [tableState.pagination, tableState.search, tableState.filters]);
const handleTableStateChange = (newState: Partial<TableState>) => {
setTableState(prev => ({ ...prev, ...newState }));
};
const columns: ColumnDef<Contact>[] = [
{ accessorKey: 'name', header: 'Name', enableHiding: false },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'status', header: 'Status' },
];
return (
<DataTable
data={data}
columns={columns}
total={total}
dataType="contact"
isLoadingBody={isLoading}
tableState={tableState}
setTableState={handleTableStateChange}
pageSize={tableState.pagination.pageSize}
currentPage={tableState.pagination.pageIndex}
/>
);
}The TableMainHeaderCell component adds a select-all checkbox to the primary column header. Combined with actionBarActions, users can select rows and perform bulk operations.
Row selection with select-all header checkbox and bulk action bar
Showing 10 of 0 user
import { useMemo, useState, useEffect } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import {
DataTable, Box, Button, ButtonGroup, IconButton,
DropdownMenu, DropdownMenuItem, DropdownMenuContent,
DropdownMenuTrigger, TableCellActions, TableMainHeaderCell,
Badge, Avatar, AvatarFallback,
} from '@thrivecart/ui';
import type { TableState } from '@thrivecart/ui';
import { MdMoreHoriz, MdDelete, MdEmail, MdPersonAdd,
MdOutlineEdit, MdOutlineEmail, MdOutlineDelete,
MdOutlinePersonAdd, MdPersonOutline } from 'react-icons/md';
interface Contact {
id: string;
email: string;
name: string;
status: string;
list_name: string;
}
function ContactsWithCheckbox() {
const [data, setData] = useState<Contact[]>([]);
const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [columnVisibility, setColumnVisibility] = useState({});
const [tableState, setTableState] = useState<TableState>({
pagination: { pageIndex: 0, pageSize: 10 },
sorting: [],
filters: {},
columns: [],
search: '',
view: 'list',
});
// Fetch data whenever tableState changes
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const params = new URLSearchParams({
page: String(tableState.pagination.pageIndex),
limit: String(tableState.pagination.pageSize),
search: tableState.search || '',
});
const res = await fetch(`/api/contacts?${params}`);
const json = await res.json();
setData(json.data);
setTotal(json.total);
setIsLoading(false);
};
fetchData();
}, [tableState.pagination, tableState.search, tableState.filters]);
const columns = useMemo<ColumnDef<Contact>[]>(() => [
{
id: 'email',
accessorKey: 'email',
// TableMainHeaderCell renders the select-all checkbox
header: ({ table }) => (
<TableMainHeaderCell label="Email" columnIndex={0} table={table} />
),
cell: (info) => {
const contact = info.row.original;
return (
<div className="flex items-center gap-1.5 w-full group/row">
<Avatar size="sm">
<AvatarFallback>
{contact.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<span className="text-ink-dark font-medium truncate block">
{contact.email}
</span>
</div>
<div className="flex-shrink-0 opacity-0 group-hover/row:opacity-100 transition-opacity">
<TableCellActions row={info.row} table={info.table}>
<ButtonGroup attached={true} size="sm">
<Button variant="secondary">View</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton variant="secondary" size="sm"
aria-label="More" icon={<MdMoreHoriz />} />
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-52">
<DropdownMenuItem><MdPersonOutline /> View Profile</DropdownMenuItem>
<DropdownMenuItem><MdOutlineEdit /> Edit</DropdownMenuItem>
<DropdownMenuItem><MdOutlineEmail /> Send Email</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
<MdOutlineDelete /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</TableCellActions>
</div>
</div>
);
},
size: 400,
enableHiding: false,
},
{
accessorKey: 'name',
header: 'Name',
meta: { label: 'Name', group: 'Contact Info' },
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<Badge variant={row.original.status === 'active' ? 'success' : 'default'}>
{row.original.status}
</Badge>
),
meta: { label: 'Status', group: 'Contact Info' },
},
{
accessorKey: 'list_name',
header: 'List',
meta: { label: 'List', group: 'Contact Info' },
},
], []);
return (
<DataTable<Contact>
columns={columns}
data={data}
total={total}
dataType="contact"
isLoadingBody={isLoading}
tableState={tableState}
setTableState={(s) => setTableState(prev => ({ ...prev, ...s }))}
onColumnVisibilityChange={setColumnVisibility}
columnVisibility={columnVisibility}
pageSize={tableState.pagination.pageSize}
currentPage={tableState.pagination.pageIndex}
filters={[
{
id: 'lists',
label: 'List(s)',
type: 'multiselect',
placeholder: 'Select lists',
searchable: true,
options: [
{ label: 'Newsletter', value: '1' },
{ label: 'VIP Members', value: '2' },
],
},
{
id: 'dateRange',
label: 'Sign Up Date',
type: 'date',
mode: 'range',
},
]}
actionBarActions={(selectedContacts) => (
<ButtonGroup attached={false} size="sm">
<Button variant="secondary" size="sm" leftIcon={<MdEmail />}>
Send Email
</Button>
<Button variant="secondary" size="sm" leftIcon={<MdPersonAdd />}>
Add to List
</Button>
<Button variant="destructive" size="sm" leftIcon={<MdDelete />}>
Delete
</Button>
<IconButton variant="secondary" size="sm" icon={<MdMoreHoriz />} />
</ButtonGroup>
)}
/>
);
}Show skeleton rows while data is being fetched
Showing 10 of 0 contact
<DataTable
data={[]}
columns={columns}
isLoadingBody={true}
dataType="contact"
/>Show contextual message when no data matches
Showing 10 of 0 contact
<DataTable
data={[]}
columns={columns}
dataType="contact"
/>A full production-style contacts table combining row selection, column visibility, filters, bulk actions, and pinned columns.
Full-featured table with search, filters, column visibility, row actions, and bulk actions
Showing 10 of 5 contact
import { useState, useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import {
DataTable, Button, ButtonGroup, IconButton, Badge,
Avatar, AvatarFallback,
DropdownMenu, DropdownMenuItem, DropdownMenuContent,
DropdownMenuTrigger, TableCellActions, TableMainHeaderCell,
} from '@thrivecart/ui';
import type { TableState } from '@thrivecart/ui';
import {
MdMoreHoriz, MdDelete, MdEmail, MdPersonAdd,
MdOutlineEdit, MdOutlineEmail, MdOutlineDelete,
MdOutlinePersonAdd, MdPersonOutline,
} from 'react-icons/md';
function ContactsList() {
const [tableState, setTableState] = useState<TableState>({
pagination: { pageIndex: 0, pageSize: 10 },
sorting: [], filters: {}, columns: [], search: '', view: 'list',
});
const [columnVisibility, setColumnVisibility] = useState({
phone: true, list_name: true, created_at: false,
});
const columns = useMemo<ColumnDef<Contact>[]>(() => [
{
id: 'email',
accessorKey: 'email',
header: ({ table }) => (
<TableMainHeaderCell label="Email" columnIndex={0} table={table} />
),
cell: (info) => {
const contact = info.row.original;
return (
<div className="flex items-center gap-1.5 w-full group/row">
<Avatar size="sm">
<AvatarFallback>
{contact.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<span className="text-ink-dark font-medium truncate block">
{contact.email}
</span>
</div>
<TableCellActions row={info.row} table={info.table}>
<ButtonGroup attached={true} size="sm">
<Button variant="secondary">View</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton variant="secondary" size="sm"
aria-label="More" icon={<MdMoreHoriz />} />
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-52">
<DropdownMenuItem><MdPersonOutline /> View Profile</DropdownMenuItem>
<DropdownMenuItem><MdOutlineEdit /> Edit</DropdownMenuItem>
<DropdownMenuItem><MdOutlineEmail /> Send Email</DropdownMenuItem>
<DropdownMenuItem><MdOutlinePersonAdd /> Add to List</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
<MdOutlineDelete /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</TableCellActions>
</div>
);
},
size: 400,
enableHiding: false,
},
{ accessorKey: 'name', header: 'Name', meta: { label: 'Name', group: 'Contact Info' } },
{ accessorKey: 'phone', header: 'Phone', meta: { label: 'Phone', group: 'Contact Info' } },
{
accessorKey: 'status', header: 'Status',
cell: ({ row }) => (
<Badge variant={row.original.status === 'active' ? 'success' : 'default'}>
{row.original.status}
</Badge>
),
meta: { label: 'Status', group: 'Contact Info' },
},
{ accessorKey: 'list_name', header: 'List', meta: { label: 'List', group: 'Contact Info' } },
{
accessorKey: 'created_at', header: 'Created',
cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString(),
meta: { label: 'Created', group: 'Details' },
},
], []);
return (
<DataTable<Contact>
columns={columns}
data={contacts}
total={contacts.length}
dataType="contact"
tableState={tableState}
setTableState={(s) => setTableState(prev => ({ ...prev, ...s }))}
onColumnVisibilityChange={setColumnVisibility}
columnVisibility={columnVisibility}
columnPinning={{ left: ['email'] }}
filters={[
{
id: 'lists', label: 'List(s)', type: 'multiselect',
placeholder: 'Select lists', searchable: true,
options: [
{ label: 'Newsletter', value: 'newsletter' },
{ label: 'VIP Members', value: 'vip' },
],
},
{ id: 'dateRange', label: 'Sign Up Date', type: 'date', mode: 'range' },
]}
actionBarActions={(selected) => (
<ButtonGroup attached={false} size="sm">
<Button variant="secondary" leftIcon={<MdEmail />}>Send Email</Button>
<Button variant="secondary" leftIcon={<MdPersonAdd />}>Add to List</Button>
<Button variant="destructive" leftIcon={<MdDelete />}>Delete</Button>
<IconButton variant="secondary" size="sm" icon={<MdMoreHoriz />} />
</ButtonGroup>
)}
/>
);
}Columns follow the TanStack Table ColumnDef format:
import type { ColumnDef } from '@tanstack/react-table';
const columns: ColumnDef<YourDataType>[] = [
{
id: 'uniqueId', // Unique column ID (optional if accessorKey is set)
accessorKey: 'fieldName', // Property name on the data object
header: 'Display Name', // String or render function for header
cell: ({ row }) => ..., // Custom cell renderer (optional)
enableSorting: true, // Allow sorting (default: true)
enableHiding: true, // Allow toggling visibility (default: true)
size: 200, // Column width in pixels
meta: {
label: 'Field Label', // Label shown in column visibility menu
group: 'Group Name', // Group in column visibility menu
},
},
];Enhance data presentation with avatars, badges, formatted dates, and links. Raw data is harder to scan.
Use the dataType prop (e.g., "contact") so empty states say "No contacts found" instead of generic text.
Set isLoadingBody={true} while fetching. Skeleton rows reassure users that data is on the way.
Set enableHiding: false on the primary identifier column (e.g., email) so users always have context.
Start with 4-6 visible columns. Let users toggle additional columns via the column menu.
Always paginate when dealing with more than 50 rows. Use server-side pagination for datasets over 1,000 rows.
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | — | Array of row data objects |
columns | ColumnDef<T>[] | — | TanStack Table column definitions |
dataType | string | 'Item' | Data label for empty states and pagination text |
total | number | — | Total records (for server-side pagination) |
pageSize | number | 10 | Rows per page |
currentPage | number | 0 | Current page index (0-based) |
isLoadingBody | boolean | false | Show skeleton loading in table body |
isLoadingHeaders | boolean | false | Show skeleton loading for entire table |
tableState | TableState | — | Controlled table state object |
setTableState | (state: Partial<TableState>) => void | — | Callback when table state changes |
searchPlaceholder | string | 'Search...' | Placeholder for the search input |
hideSearch | boolean | false | Hide the search input |
hideColumnsMenu | boolean | false | Hide column visibility toggle |
hideViewMenu | boolean | false | Hide view switcher |
filters | TableFilter[] | — | Filter configurations (see below) |
columnVisibility | Record<string, boolean> | {} | Controlled column visibility state |
onColumnVisibilityChange | (visibility) => void | — | Callback when column visibility changes |
onRowSelectionChange | (selectedRows: T[]) => void | — | Callback when row selection changes |
actionBarActions | ReactNode | ((rows: T[]) => ReactNode) | — | Bulk action buttons for selected rows |
columnPinning | { left?: string[]; right?: string[] } | — | Pin columns to left or right edges (sticky) |
maxHeight | string | 'var(--data-table-content-height)' | Max height of table body |
className | string | — | Additional className |
interface TableState {
pagination: { pageIndex: number; pageSize: number };
sorting: SortingState;
filters: Record<string, any>;
columns: ColumnOrderState;
search: string;
view: 'list' | 'grid';
}// Single select filter
{ id: 'status', label: 'Status', type: 'select', options: [...] }
// Multi-select filter with search
{ id: 'lists', label: 'Lists', type: 'multiselect', searchable: true, options: [...] }
// Date range filter
{ id: 'dateRange', label: 'Date', type: 'date', mode: 'range' }
// Boolean filter
{ id: 'verified', label: 'Verified', type: 'boolean', trueLabel: 'Yes', falseLabel: 'No' }Keyboard Navigation
DataTable supports full keyboard navigation. Use Tab to move between interactive elements, Enter or Space to activate buttons, and arrow keys within dropdowns.
table, rowgroup, row, columnheader, cell)aria-sortaria-checkedaria-live regions