Skip to main content
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>

Related Blocks

Component Docs

FAQ