nili/ui
All components

Table

Overview
29 exports12 variant axes31 props4 files
import { SortDirection, Table, TableAlign, TableBody, TableCell, TableFooter } from '@nili/ui';

This is the library’s own overview page, rendered live. Switch the language or the theme in the header and it follows.

Basic

A real table element, not divs with row roles. Screen readers give a real table a navigation mode — move by cell, hear the column header on every jump — and none of that survives the rewrite. label names it; a table with no name is announced as “table” and nothing else.

CustomerAmountStatus
Acme Co.1,200paid
Globex480pending
Initech3,150overdue
Umbrella90paid
import {
  Table, TableHeader, TableBody, TableRow, TableHead, TableCell, Badge,
} from "@nili/ui"

export function Example() {
  return (
    <Table label="Invoices">
      <TableHeader>
        <TableRow>
          <TableHead>Customer</TableHead>
          <TableHead align="end">Amount</TableHead>
          <TableHead>Status</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((row) => (
          <TableRow key={row.id}>
            <TableCell header>{row.customer}</TableCell>
            <TableCell align="end" numeric>{row.amount}</TableCell>
            <TableCell>
              <Badge appearance="light" color="green" dot>Paid</Badge>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Row height and caption

Two row heights — X-Large (64) and Large (48). A visible caption names the table, so label is not needed as well.

size=lg, with a caption
Invoices from the last 30 days
CustomerAmount
Acme Co.1,200
Globex480
Initech3,150
import { Table } from "@nili/ui"

export function Example() {
  return (
    <>
      <Table size="xl" label="Invoices">…</Table>
      <Table size="lg" caption={t('sizes.caption')}>…</Table>
    </>
  )
}

Cell priority

How loudly a column reads. leading is the row's identity, passive is metadata the eye should skip, and none drops the type styling entirely so the cell can host a control.

CustomerEmailUpdatedActions
A
Acme Co.
billing@acme.com2 days ago
G
Globex
ap@globex.io2 days ago
I
Initech
finance@initech.dev2 days ago
import { Table, TableCell } from "@nili/ui"

export function Example() {
  return (
    <TableRow>
      <TableCell priority="leading" header>Acme Co.</TableCell>
      <TableCell priority="regular">billing@acme.com</TableCell>
      <TableCell priority="passive">Updated 2 days ago</TableCell>
      <TableCell priority="none">
        <CompactButton aria-label="More"><Icon icon={RiMore2Fill} /></CompactButton>
      </TableCell>
    </TableRow>
  )
}

Sorting

sortable renders the sort button and manages aria-sort on the cell. It lands on the sorted column only — “none” on every other column is noise a screen reader reads aloud.

Initech3,150
Acme Co.1,200
Globex480
Umbrella90
import { TableHead } from "@nili/ui"

export function Example() {
  const [sort, setSort] = useState({ key: "amount", direction: "desc" })

  return (
    <TableHead
      align="end"
      sortable
      sortDirection={sort.key === "amount" ? sort.direction : null}
      sortLabel={t('sorting.byAmount')}
      onSort={(direction) => setSort({ key: "amount", direction })}
    >
      Amount
    </TableHead>
  )
}

Selection, sticky header and footer

selected highlights a row and adds aria-selected. interactive is only a hover affordance — a row whose onClick opens a record is invisible to a screen reader, so put a real link or button in a cell and let the row highlight follow it.

CustomerAmount
Acme Co.1,200
Globex480
Initech3,150
Umbrella90
1 of 4 selected4,920
import { Table, TableRow, TableHead, TableCell, Checkbox } from "@nili/ui"

export function Example() {
  return (
    <Table label="Invoices" stickyHeader>
      <TableHeader sticky>
        <TableRow>
          <TableHead state="empty"><Checkbox aria-label="Select all" /></TableHead>
          <TableHead>Customer</TableHead>
        </TableRow>
      </TableHeader>

      <TableBody>
        <TableRow selected interactive>
          <TableCell priority="none"><Checkbox aria-label="Select row" checked /></TableCell>
          <TableCell header>Globex</TableCell>
        </TableRow>
      </TableBody>

      <TableFooter>
        <TableRow>
          <TableCell colSpan={2}>4 invoices</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}