Components
HeroUI v3 Table — data table with sorting, selection, custom cells, expandable rows, pagination, column resizing, empty state and async loading.
npx @21st-dev/cli@beta add hero_ui/heroui-tableLoading preview...
"use client"
import type { Selection, SortDescriptor } from "@heroui/react"
import { Table } from "@/components/ui/heroui-table"
import { Avatar, Button, Checkbox, Chip } from "@heroui/react"
import { Copy, Eye, Pencil, Trash2 } from "lucide-react"
import { useMemo, useState } from "react"
interface User {
id: number
name: string
image_url: string
role: string
status: "Active" | "Inactive" | "On Leave"
email: string
}
const statusColorMap: Record<string, "success" | "danger" | "warning"> = {
Active: "success",
Inactive: "danger",
"On Leave": "warning",
}
const users: User[] = [
{
email: "kate@acme.com",
id: 4586932,
image_url: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg",
name: "Kate Moore",
role: "Chief Executive Officer",
status: "Active",
},
{
email: "john@acme.com",
id: 5273849,
image_url: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg",
name: "John Smith",
role: "Chief Technology Officer",
status: "Active",
},
{
email: "sara@acme.com",
id: 7492836,
image_url: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
name: "Sara Johnson",
role: "Chief Marketing Officer",
status: "On Leave",
},
{
email: "michael@acme.com",
id: 8293746,
image_url: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg",
name: "Michael Brown",
role: "Chief Financial Officer",
status: "Active",
},
{
email: "emily@acme.com",
id: 1234567,
image_url: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg",
name: "Emily Davis",
role: "Product Manager",
status: "Inactive",
},
]
export default function CustomCells() {
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set())
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "name",
direction: "ascending",
})
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) => {
const col = sortDescriptor.column as keyof User
const first = String(a[col])
const second = String(b[col])
let cmp = first.localeCompare(second)
if (sortDescriptor.direction === "descending") {
cmp *= -1
}
return cmp
})
}, [sortDescriptor])
return (
<div className="w-full p-8">
<Table>
<Table.ScrollContainer>
<Table.Content
aria-label="Table with custom cells"
className="min-w-[800px]"
selectedKeys={selectedKeys}
selectionMode="multiple"
sortDescriptor={sortDescriptor}
onSelectionChange={setSelectedKeys}
onSortChange={setSortDescriptor}
>
<Table.Header>
<Table.Column className="pr-0">
<Checkbox aria-label="Select all" slot="selection">
<Checkbox.Content>
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
</Checkbox.Content>
</Checkbox>
</Table.Column>
<Table.Column allowsSorting isRowHeader className="after:hidden" id="id">
{({ sortDirection }) => (
<Table.SortableColumnHeader sortDirection={sortDirection}>
Worker ID
</Table.SortableColumnHeader>
)}
</Table.Column>
<Table.Column allowsSorting id="name">
{({ sortDirection }) => (
<Table.SortableColumnHeader sortDirection={sortDirection}>
Member
</Table.SortableColumnHeader>
)}
</Table.Column>
<Table.Column allowsSorting id="role">
{({ sortDirection }) => (
<Table.SortableColumnHeader sortDirection={sortDirection}>
Role
</Table.SortableColumnHeader>
)}
</Table.Column>
<Table.Column allowsSorting id="status">
{({ sortDirection }) => (
<Table.SortableColumnHeader sortDirection={sortDirection}>
Status
</Table.SortableColumnHeader>
)}
</Table.Column>
<Table.Column className="text-end">Actions</Table.Column>
</Table.Header>
<Table.Body>
{sortedUsers.map((user) => (
<Table.Row key={user.id} id={user.id}>
<Table.Cell className="pr-0">
<Checkbox aria-label={`Select ${user.name}`} slot="selection" variant="secondary">
<Checkbox.Content>
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
</Checkbox.Content>
</Checkbox>
</Table.Cell>
<Table.Cell className="font-medium">
<div className="flex items-center gap-2">
#{user.id.toString()}{" "}
<Button isIconOnly size="sm" variant="ghost">
<Copy className="size-4 text-muted" />
</Button>
</div>
</Table.Cell>
<Table.Cell>
<div className="flex items-center gap-3">
<Avatar size="sm">
<Avatar.Image src={user.image_url} />
<Avatar.Fallback>
{user.name
.split(" ")
.map((n) => n[0])
.join("")}
</Avatar.Fallback>
</Avatar>
<div className="flex flex-col">
<span className="text-xs">{user.name}</span>
<span className="text-xs text-muted">{user.email}</span>
</div>
</div>
</Table.Cell>
<Table.Cell className="min-w-52">{user.role}</Table.Cell>
<Table.Cell className="min-w-25">
<Chip color={statusColorMap[user.status]} size="sm" variant="soft">
{user.status}
</Chip>
</Table.Cell>
<Table.Cell>
<div className="flex items-center gap-1">
<Button isIconOnly size="sm" variant="tertiary">
<Eye className="size-4" />
</Button>
<Button isIconOnly size="sm" variant="tertiary">
<Pencil className="size-4" />
</Button>
<Button isIconOnly size="sm" variant="danger-soft">
<Trash2 className="size-4" />
</Button>
</div>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Content>
</Table.ScrollContainer>
</Table>
</div>
)
}
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...