Use Case
Build a Data Table with React and Tailwind CSS v4
Build an accessible data table with sorting, filtering, and pagination using Ninna UI's Table component and Tailwind CSS v4.
Accessible table semantics
The Table component from @ninna-ui/data-display gives you accessible table semantics out of the box: proper <table>, <thead>, <tbody>, <th> elements with scope attributes. Screen readers navigate tables correctly when the markup is semantic.
1. Basic table
Start with a simple table structure:
import { Table } from "@ninna-ui/data-display";
<Table>
<Table.Head>
<Table.Row>
<Table.Header>Name</Table.Header>
<Table.Header>Status</Table.Header>
<Table.Header>Role</Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
<Table.Row>
<Table.Cell>Jane Doe</Table.Cell>
<Table.Cell>Active</Table.Cell>
<Table.Cell>Admin</Table.Cell>
</Table.Row>
</Table.Body>
</Table>2. Sortable headers
Add clickable headers with sort indicators. Use aria-sort on the header to communicate sort state to screen readers:
<Table.Header
aria-sort={sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none"}
onClick={() => toggleSort("name")}
className="cursor-pointer select-none"
>
Name {sortKey === "name" && (sortDir === "asc" ? "↑" : "↓")}
</Table.Header>3. Filter input
Add a search input above the table for filtering:
import { Input } from "@ninna-ui/forms";
import { Search } from "lucide-react";
<div className="mb-4">
<Input
placeholder="Search..."
value={search}
onChange={(e) => setSearch(e.target.value)}
startIcon={<Search className="size-4" />}
/>
</div>4. Pagination
Use Button components for pagination controls:
import { Button } from "@ninna-ui/primitives";
import { HStack } from "@ninna-ui/layout";
<HStack gap="2" className="mt-4">
<Button variant="outline" size="sm" onClick={prevPage} disabled={page === 1}>Previous</Button>
<Text>Page {page} of {totalPages}</Text>
<Button variant="outline" size="sm" onClick={nextPage} disabled={page === totalPages}>Next</Button>
</HStack>