Data table
Search, sort, select rows and page through results.
Built with
- Badge
- Button
- EmptyState
- IconButton
- SearchField
- Table
- TableBody
- TableCell
- TableFooter
- TableHeader
- TableHeaderCell
- TableRow
- TableToolbar
People
12 of 12 people
| Employee | Role | Team | Status | Start date | Actions | |
|---|---|---|---|---|---|---|
Ahmed Razaahmed.raza@northwind.io | Engineering managerLahore | Engineering | Remote | 26 Aug 2019 | ||
Ayesha Khanayesha.khan@northwind.io | Senior product designerLondon | Design | Active | 15 Mar 2021 | ||
Chloe Martinchloe.martin@northwind.io | Data analystParis | Finance | Remote | 2 Nov 2020 | ||
Daniel Okafordaniel.okafor@northwind.io | Backend engineerLagos | Engineering | Active | 11 Jul 2022 | ||
Lena Novaklena.novak@northwind.io | Content designerPrague | Design | On leave | 4 Oct 2021 |
1–5 of 12
Rows per page
data-table.tsx
"use client";
import { useMemo, useState, type ComponentProps } from "react";
import { LuDownload, LuEllipsis, LuUserPlus, LuUsers } from "react-icons/lu";
import {
Badge,
Button,
EmptyState,
IconButton,
SearchField,
Table,
TableBody,
TableCell,
TableFooter,
TableHeader,
TableHeaderCell,
TableRow,
TableToolbar,
} from "@nexera-ui/react";
// Sample data
interface Person {
id: string;
name: string;
initials: string;
role: string;
team: string;
email: string;
location: string;
status: "Active" | "On leave" | "Remote" | "Invited";
}
const PEOPLE: Person[] = [
{
id: "p1",
name: "Ayesha Khan",
initials: "AK",
role: "Senior product designer",
team: "Design",
email: "ayesha.khan@northwind.io",
location: "London",
status: "Active",
},
{
id: "p2",
name: "Sara Malik",
initials: "SM",
role: "Payroll specialist",
team: "People Ops",
email: "sara.malik@northwind.io",
location: "Lisbon",
status: "On leave",
},
{
id: "p3",
name: "Ahmed Raza",
initials: "AR",
role: "Engineering manager",
team: "Engineering",
email: "ahmed.raza@northwind.io",
location: "Lahore",
status: "Remote",
},
{
id: "p4",
name: "Mia Jensen",
initials: "MJ",
role: "Product manager",
team: "Product",
email: "mia.jensen@northwind.io",
location: "Copenhagen",
status: "Active",
},
{
id: "p5",
name: "Luis Ortega",
initials: "LO",
role: "Frontend engineer",
team: "Engineering",
email: "luis.ortega@northwind.io",
location: "Madrid",
status: "Active",
},
{
id: "p6",
name: "Yuki Tanaka",
initials: "YT",
role: "Recruiter",
team: "People Ops",
email: "yuki.tanaka@northwind.io",
location: "Tokyo",
status: "Active",
},
{
id: "p7",
name: "Chloe Martin",
initials: "CM",
role: "Data analyst",
team: "Finance",
email: "chloe.martin@northwind.io",
location: "Paris",
status: "Remote",
},
{
id: "p8",
name: "Omar Haddad",
initials: "OH",
role: "Support lead",
team: "Customer",
email: "omar.haddad@northwind.io",
location: "Dubai",
status: "Invited",
},
];
type SortDescriptor = NonNullable<ComponentProps<typeof Table>["sortDescriptor"]>;
type Selection = "all" | Set<string | number>;
type Row = Person & { start: string };
const STARTS = [
"2021-03-15",
"2023-01-09",
"2019-08-26",
"2022-05-03",
"2024-02-12",
"2023-09-18",
"2020-11-02",
"2026-10-05",
];
const ROWS: Row[] = [
...PEOPLE.map((person, i) => ({ ...person, start: STARTS[i] ?? "2024-01-01" })),
{
id: "p9",
name: "Daniel Okafor",
initials: "DO",
role: "Backend engineer",
team: "Engineering",
email: "daniel.okafor@northwind.io",
location: "Lagos",
status: "Active",
start: "2022-07-11",
},
{
id: "p10",
name: "Priya Nair",
initials: "PN",
role: "Finance manager",
team: "Finance",
email: "priya.nair@northwind.io",
location: "Bengaluru",
status: "Active",
start: "2018-04-23",
},
{
id: "p11",
name: "Tom Becker",
initials: "TB",
role: "Customer success manager",
team: "Customer",
email: "tom.becker@northwind.io",
location: "Berlin",
status: "Remote",
start: "2025-06-30",
},
{
id: "p12",
name: "Lena Novak",
initials: "LN",
role: "Content designer",
team: "Design",
email: "lena.novak@northwind.io",
location: "Prague",
status: "On leave",
start: "2021-10-04",
},
];
const TONE = { Active: "success", "On leave": "leave", Remote: "remote", Invited: "info" } as const;
const date = new Intl.DateTimeFormat("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
timeZone: "UTC",
});
/** Data table: search, sort, select and page through Northwind's people, with the toolbar and footer that belong to the table. */
export default function DataTable() {
const [query, setQuery] = useState("");
const [sort, setSort] = useState<SortDescriptor>({ column: "name", direction: "ascending" });
const [selected, setSelected] = useState<Selection>(new Set());
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(5);
const rows = useMemo(() => {
const q = query.trim().toLowerCase();
const found = ROWS.filter(
(r) => !q || `${r.name} ${r.role} ${r.team} ${r.location}`.toLowerCase().includes(q),
);
const key = sort.column as "name" | "team" | "start";
found.sort((a, b) => a[key].localeCompare(b[key]) * (sort.direction === "descending" ? -1 : 1));
return found;
}, [query, sort]);
const pageCount = Math.max(1, Math.ceil(rows.length / pageSize));
const current = Math.min(page, pageCount);
const visible = rows.slice((current - 1) * pageSize, current * pageSize);
const count = selected === "all" ? rows.length : selected.size;
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-6xl gap-4">
<header className="grid gap-1">
<h2 id="people-table-title" className="text-heading-h2 text-primary">
People
</h2>
<p className="text-body-default text-secondary" aria-live="polite">
{rows.length} of {ROWS.length} people{count > 0 ? ` · ${count} selected` : ""}
</p>
</header>
<div className="grid min-w-0 overflow-hidden rounded-lg border border-default bg-surface">
<TableToolbar
aria-label="People table tools"
search={
<SearchField
aria-label="Search people"
placeholder="Search name, role or team"
value={query}
onChange={(v) => {
setQuery(v);
setPage(1);
}}
/>
}
actions={
<>
<Button variant="secondary" leadingIcon={<LuDownload />} isDisabled={count === 0}>
Export
</Button>
<Button leadingIcon={<LuUserPlus />}>Add person</Button>
</>
}
/>
<Table
aria-labelledby="people-table-title"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
sortDescriptor={sort}
onSortChange={setSort}
>
<TableHeader>
<TableHeaderCell id="name" isRowHeader allowsSorting label="Employee" />
<TableHeaderCell id="role" label="Role" />
<TableHeaderCell id="team" allowsSorting label="Team" />
<TableHeaderCell id="status" label="Status" />
<TableHeaderCell id="start" allowsSorting label="Start date" />
<TableHeaderCell id="actions" align="right" label="Actions" />
</TableHeader>
<TableBody
items={visible}
renderEmptyState={() => (
<div className="px-6 py-12">
<EmptyState
icon={<LuUsers />}
title={`No one matches “${query}”`}
description="Check the spelling, or search by team or role."
headingLevel={3}
secondaryAction={
<Button
variant="secondary"
size="sm"
onPress={() => {
setQuery("");
}}
>
Clear search
</Button>
}
/>
</div>
)}
>
{(row) => (
<TableRow id={row.id}>
<TableCell
type="avatar"
name={row.name}
email={row.email}
initials={row.initials}
/>
<TableCell type="text-and-subtext" primary={row.role} secondary={row.location} />
<TableCell>{row.team}</TableCell>
<TableCell type="badge">
<Badge label={row.status} tone={TONE[row.status]} />
</TableCell>
<TableCell>{date.format(new Date(`${row.start}T00:00:00Z`))}</TableCell>
<TableCell type="actions">
<IconButton
variant="ghost"
aria-label={`More actions for ${row.name}`}
icon={<LuEllipsis />}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TableFooter
aria-label="People pages"
page={current}
pageCount={pageCount}
onPageChange={setPage}
pageSize={pageSize}
pageSizes={[5, 10, 20]}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1);
}}
range={
rows.length === 0
? "No results"
: `${(current - 1) * pageSize + 1}–${Math.min(current * pageSize, rows.length)} of ${rows.length}`
}
/>
</div>
</div>
</div>
);
}
More lists, tables and records blocks
- Bulk actionsA floating bar that acts on the selected rows.
- Filter barActive filters as removable chips above results.
- Filters drawerDetailed filters in a side panel.
- People directorySearchable list of people with presence.
- Record details drawerOpen a row to see details without leaving the list.
- Activity feedNewest first, with a 'new items' pill and paging.
- File browserFolders and files in a tree.
- Keyboard shortcutsA modal listing every shortcut.