Table
A powerful data table component built with TanStack Table v8. Supports sorting, filtering, pagination, row selection, and many customization options.
← Back to componentsExamples
Basic
A simple table with user data, avatars, and basic styling.
Name | Email | Role | Status | |
|---|---|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin | Active | |
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor | Active | |
Brice Steiner @brice | brice@untitledui.com | Viewer | Pending |
const userColumns = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => (
<div className="flex items-center gap-3">
<Avatar src={row.original.avatar} alt={row.original.name} size="md" />
<div>
<p className="font-medium">{row.original.name}</p>
<p className="text-sm text-muted-foreground">{row.original.username}</p>
</div>
</div>
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "role",
header: "Role",
cell: ({ getValue }) => (
<Badge variant="secondary">{getValue() as string}</Badge>
),
},
];
<Table data={usersData} columns={userColumns} size="md" />With row selection
Enable single or multiple row selection with checkboxes.
Name | Email | Role | |
|---|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin | |
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor | |
Brice Steiner @brice | brice@untitledui.com | Viewer |
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
<Table
data={usersData}
columns={userColumns}
enableRowSelection
enableMultiRowSelection
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
size="md"
/>With sorting
Click on column headers to sort data in ascending or descending order.
Name | Email | Role | Last seen |
|---|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin | 2 hours ago |
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor | 1 day ago |
Brice Steiner @brice | brice@untitledui.com | Viewer | 3 days ago |
const [sorting, setSorting] = useState<SortingState>([]);
<Table
data={usersData}
columns={userColumns}
enableSorting
sorting={sorting}
onSortingChange={setSorting}
size="md"
/>Complex data
Table with complex data types including progress bars, team avatars, and custom badges.
Project | Status | Progress | Team | Budget |
|---|---|---|---|---|
Website Redesign | In Progress | 75% | $25,000.00 | |
Mobile App | Completed | 100% | $45,000.00 |
const projectColumns = [
projectColumnHelper.accessor("name", {
header: "Project",
cell: ({ getValue }) => (
<div className="font-medium">{getValue()}</div>
),
}),
projectColumnHelper.accessor("status", {
header: "Status",
cell: ({ getValue }) => {
const status = getValue();
const config = {
"Completed": { color: "success", icon: CheckCircle },
"In Progress": { color: "warning", icon: Clock },
"On Hold": { color: "gray", icon: XCircle },
"Cancelled": { color: "error", icon: XCircle }
};
const { color, icon: Icon } = config[status];
return (
<div className="flex items-center gap-2">
<Icon className="size-4" />
<Badge color={color}>{status}</Badge>
</div>
);
},
}),
projectColumnHelper.accessor("progress", {
header: "Progress",
cell: ({ getValue }) => {
const progress = getValue();
return (
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-sm text-muted-foreground">{progress}%</span>
</div>
);
},
}),
];Table card
Wrap your table in a card with header, badge, and action buttons.
Team members
24 usersManage your team members and their access levels
Name | Email | Role | ||
|---|---|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin | ||
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor | ||
Brice Steiner @brice | brice@untitledui.com | Viewer |
<TableCard
title="Team members"
badge="24 users"
description="Manage your team members and their access levels"
contentTrailing={
<div className="flex gap-2">
<Button variant="secondary" size="sm" leftIcon={<DownloadCloud01 />}>
Download
</Button>
<Button size="sm" leftIcon={<Plus />}>
Add member
</Button>
</div>
}
>
<Table
data={usersData}
columns={userColumns}
enableRowSelection
enableSorting
size="md"
/>
</TableCard>Alternating rows
Add zebra striping to table rows for better readability.
Name | Email | Role | Department |
|---|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin | Design |
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor | Engineering |
Brice Steiner @brice | brice@untitledui.com | Viewer | Marketing |
<Table
data={usersData}
columns={userColumns}
alternatingRows={true}
size="md"
/>With pagination
Add pagination controls for large datasets.
Invoices
3 invoicesInvoice | Customer | Amount | Status | Due date |
|---|---|---|---|---|
| INV-001 | Acme Corp | $2,500.00 | Paid | 2/15/2024 |
| INV-002 | TechStart Ltd | $1,800.00 | Pending | 2/20/2024 |
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 2,
});
<TableCard title="Invoices" badge="247 invoices">
<Table
data={invoicesData}
columns={invoiceColumns}
pagination={pagination}
onPaginationChange={setPagination}
size="md"
/>
{/* Custom pagination footer */}
<Pagination
page={pagination.pageIndex + 1}
total={Math.ceil(invoicesData.length / pagination.pageSize)}
onPageChange={(page) => setPagination(prev => ({
...prev,
pageIndex: page - 1
}))}
/>
</TableCard>Empty state
Custom empty state when no data is available.
Name | Email | Role |
|---|---|---|
No member foundYour search did not match. Please try again or create add a new member. | ||
<Table
data={[]}
columns={userColumns}
emptyMessage={
<div className="text-center py-12">
<div className="mx-auto size-12 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<Users className="size-6 text-gray-400" />
</div>
<h3 className="font-medium text-gray-900 mb-1">No team members</h3>
<p className="text-sm text-quaternary mb-4">
Get started by adding your first team member.
</p>
<Button size="sm" leftIcon={<Plus />}>
Add team member
</Button>
</div>
}
size="md"
/>Sizes
Two size variants: sm and md (default).
Small
Name | Email | Role |
|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin |
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor |
Medium (default)
Name | Email | Role |
|---|---|---|
Mark Rhye @mark | mark@untitledui.com | Admin |
Phoenix Baker @phoenix | phoenix@untitledui.com | Editor |
<Table data={usersData} columns={userColumns} size="sm" />
<Table data={usersData} columns={userColumns} size="md" />API Reference
Table
| Props | Type | Default | Description |
|---|---|---|---|
| data | TData[] | - | Array of data objects to display |
| columns | ColumnDef<TData>[] | - | Column definitions |
| size? | "sm" | "md" | "md" | Table size variant |
| enableRowSelection? | boolean | false | Enable row selection |
| enableMultiRowSelection? | boolean | true | Allow multiple row selection |
| enableSorting? | boolean | true | Enable column sorting |
| alternatingRows? | boolean | false | Add zebra striping |
| bordered? | boolean | true | Show borders between rows |
| emptyMessage? | ReactNode | "No results." | Message shown when no data |
TableCard
| Props | Type | Default | Description |
|---|---|---|---|
| title | string | - | Card header title |
| badge? | ReactNode | - | Badge next to title |
| description? | string | - | Description below title |
| contentTrailing? | ReactNode | - | Content on the right side |