Table
Rows of data that compare along columns: members, invoices, deployments. Rows run edge to edge with a divider under each, header text is secondary, and a body row takes the neutral hover overlay. Cells hold any component: an Avatar and a name, a Badge, a Checkbox to pick the row, a DropdownMenu of row actions. Headers can sort, a TableToolbar above and below holds search, actions, the count and Pagination, and TableEmpty says when there are no rows. TableSelection adds row picking: a checkbox per row, one in the header that follows them, and a bar that counts and names the picked rows.
Preview
import { IconDots } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead onSort={sortBy}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell>Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell>Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>Usage
When to use
- Rows of data the user compares along columns: members, invoices, deployments.
- Numbers that line up down a column, such as amounts or counts.
- Rows the user picks and then acts on together, such as removing several members, with TableSelection.
When not to use
- A single list of items with one or two lines each. Use a list of rows instead.
- A grid of cards, such as projects with images. Use Card.
- Layout. Use flex or grid classes.
Variants
align
Set on TableHead and TableCell. Where the content sits in the cell. end for numbers, center only for short status marks.
import { IconDots } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead onSort={sortBy}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell>Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell>Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead onSort={sortBy}>Invoice</TableHead>
<TableHead align="center">Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell align="center">Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell align="center">Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead onSort={sortBy}>Invoice</TableHead>
<TableHead align="end">Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell align="end">Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell align="end">Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>sortDirection
Set on a sortable TableHead. How the table is sorted by this column; sets aria-sort and the caret.
import { IconDots } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead onSort={sortBy}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell>Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell>Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead sortDirection="ascending" onSort={sortBy}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell>Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell>Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table className="max-w-lg">
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead sortDirection="descending" onSort={sortBy}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell>Paid</TableCell>
<TableCell numeric>$250.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-001">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell>Due</TableCell>
<TableCell numeric>$1,200.00</TableCell>
<TableCell>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for INV-002">
<IconDots size={16} weight="regular" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>Examples
Invoices
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Table>
<TableCaption>Invoices for 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead numeric>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell>
<Badge variant="success">Paid</Badge>
</TableCell>
<TableCell numeric>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell>
<Badge variant="warning">Due</Badge>
</TableCell>
<TableCell numeric>$1,200.00</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell>Total</TableCell>
<TableCell />
<TableCell numeric>$1,450.00</TableCell>
</TableRow>
</TableFooter>
</Table>Members
Rows picked with TableSelection: the header checkbox follows the rows, and the bar above counts and names the picked ones, with the actions on them. Search and an action above the table, a sortable name, an Avatar and an email in one cell, a Badge, row actions at the end, the count with Pagination below.
import { IconDots, IconSearch } from "@tabler/icons-react"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Pagination } from "@/components/ui/pagination"
import { Table, TableActions, TableBody, TableCell, TableHead, TableHeader, TableRow, TableToolbar } from "@/components/ui/table"
import { TableRowSelect, TableSelectAll, TableSelection, TableSelectionBar } from "@/components/ui/table-selection"
<TableSelection defaultValue={["ada"]}>
<TableSelectionBar>
<Button variant="secondary" size="sm">Export</Button>
<Button variant="destructive-soft" size="sm">Remove</Button>
</TableSelectionBar>
<TableToolbar>
<Input aria-label="Search members" placeholder="Search members" leadingIcon={<IconSearch size={16} />} className="max-w-xs" />
<TableActions>
<Button>Invite</Button>
</TableActions>
</TableToolbar>
<Table aria-label="Members">
<TableHeader>
<TableRow>
<TableHead fit>
<TableSelectAll aria-label="Select all members" />
</TableHead>
<TableHead sortDirection="ascending" onSort={sortBy}>Name</TableHead>
<TableHead>Role</TableHead>
<TableHead fit>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell fit>
<TableRowSelect value="ada" label="Ada Lovelace" />
</TableCell>
<TableCell>
<div className="flex items-center gap-sm">
<Avatar alt="" color="blue">AL</Avatar>
<div className="flex flex-col">
<span className="font-medium">Ada Lovelace</span>
<span className="text-sm text-text-secondary">ada@example.com</span>
</div>
</div>
</TableCell>
<TableCell>
<Badge>Owner</Badge>
</TableCell>
<TableCell fit>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for Ada Lovelace">
<IconDots size={16} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Change role</DropdownMenuItem>
<DropdownMenuItem destructive>Remove from team</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell fit>
<TableRowSelect value="grace" label="Grace Hopper" />
</TableCell>
<TableCell>
<div className="flex items-center gap-sm">
<Avatar alt="" color="green">GH</Avatar>
<div className="flex flex-col">
<span className="font-medium">Grace Hopper</span>
<span className="text-sm text-text-secondary">grace@example.com</span>
</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline">Editor</Badge>
</TableCell>
<TableCell fit>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" iconOnly aria-label="Actions for Grace Hopper">
<IconDots size={16} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Change role</DropdownMenuItem>
<DropdownMenuItem destructive>Remove from team</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
<TableToolbar>
<span className="text-sm text-text-secondary">2 of 24 members</span>
<TableActions>
<Pagination page={1} pageCount={12} onPageChange={goToPage} />
</TableActions>
</TableToolbar>
</TableSelection>Many rows picked
Past three names the bar shows the rest as a count. max on TableSelectionBar changes how many names show.
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { TableRowSelect, TableSelectAll, TableSelection, TableSelectionBar } from "@/components/ui/table-selection"
<TableSelection defaultValue={["ada", "grace", "alan", "katherine", "linus"]}>
<TableSelectionBar>
<Button variant="destructive-soft" size="sm">Remove</Button>
</TableSelectionBar>
<Table aria-label="Members">
<TableHeader>
<TableRow>
<TableHead fit>
<TableSelectAll aria-label="Select all members" />
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell fit>
<TableRowSelect value="ada" label="Ada Lovelace" />
</TableCell>
<TableCell>Ada Lovelace</TableCell>
<TableCell>Owner</TableCell>
</TableRow>
<TableRow>
<TableCell fit>
<TableRowSelect value="grace" label="Grace Hopper" />
</TableCell>
<TableCell>Grace Hopper</TableCell>
<TableCell>Editor</TableCell>
</TableRow>
<TableRow>
<TableCell fit>
<TableRowSelect value="alan" label="Alan Turing" />
</TableCell>
<TableCell>Alan Turing</TableCell>
<TableCell>Editor</TableCell>
</TableRow>
<TableRow>
<TableCell fit>
<TableRowSelect value="katherine" label="Katherine Johnson" />
</TableCell>
<TableCell>Katherine Johnson</TableCell>
<TableCell>Viewer</TableCell>
</TableRow>
<TableRow>
<TableCell fit>
<TableRowSelect value="linus" label="Linus Torvalds" />
</TableCell>
<TableCell>Linus Torvalds</TableCell>
<TableCell>Viewer</TableCell>
</TableRow>
<TableRow>
<TableCell fit>
<TableRowSelect value="margaret" label="Margaret Hamilton" />
</TableCell>
<TableCell>Margaret Hamilton</TableCell>
<TableCell>Viewer</TableCell>
</TableRow>
</TableBody>
</Table>
</TableSelection>No results
import { Table, TableBody, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Table aria-label="Members">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableEmpty colSpan={2}>No members match your search.</TableEmpty>
</TableBody>
</Table>Next page loading
The user asked for page 2, which is still loading: page 1 stays, dimmed and marked busy. In an app, pending is the isPending of the transition that changes the page.
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
<Table pending pendingLabel="Loading page 2" aria-label="Invoices">
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead numeric>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-001</TableCell>
<TableCell numeric>$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>INV-002</TableCell>
<TableCell numeric>$150.00</TableCell>
</TableRow>
</TableBody>
</Table>Do and don't
Do
- Use numeric on every cell of a number column, the header too.
- Put the table on a surface with its own edge (a Card, a page section); the table has none.
- Name it with a caption, or aria-label when a heading above says what it is.
- Put search and the main action in a TableToolbar above the table, and the count with Pagination in one below.
- Use fit for a Checkbox column and an actions column, so the data columns get the width.
- For rows the user picks and then acts on together, wrap the table in TableSelection; it keeps the header checkbox, the rows and the bar in step.
- Put the actions on the picked rows (Export, Delete) in TableSelectionBar, as sm buttons.
- When a page, sort or filter loads from the server, change it in a transition (useTransition) and pass isPending as pending: the current rows stay until the next ones arrive. Use Skeleton rows only for the first load.
Don't
- Don't wrap each row in a Card; rows are the layout.
- Don't center text columns; start-align them.
- Don't use a table for layout or for a single list of items.
- Don't wire Checkbox cells by hand when TableSelection fits; the header checkbox and the bar drift out of step.
- Don't empty the table or swap the rows for a spinner while the next page loads; the layout jumps and the user loses their place.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| align | enum | start | center | end | start | Set on TableHead and TableCell. Where the content sits in the cell. end for numbers, center only for short status marks. |
| numeric | boolean | - | false | Set on TableHead and TableCell. A number column: end-aligned, mono, tabular figures, so digits line up. |
| fit | boolean | - | false | Set on TableHead and TableCell. The column takes only the room its content needs: a Checkbox, a row's actions. |
| sortDirection | enum | none | ascending | descending | none | Set on a sortable TableHead. How the table is sorted by this column; sets aria-sort and the caret. |
| onSort | function | - | - | Set on TableHead. Makes the header a button that sorts by the column. Sort the rows yourself, then set sortDirection. |
| selected | boolean | - | false | Set on TableRow. The row is picked with its Checkbox: the muted accent fill. |
| colSpan | number | - | - | Set on TableEmpty. How many columns the table has, so the row spans them all. |
| defaultValue | string | - | - | Set on TableSelection. Values of the rows picked on first render, as an array: ["ada"]. Each value is a TableRowSelect's value. |
| value | string | - | - | Set on TableSelection: the picked values as an array, when you control them. Pair it with onValueChange. TableRowSelect takes one value, its row's id. |
| onValueChange | function | - | - | Set on TableSelection. Called with the new array of picked values. |
| label | string | - | - | Set on TableRowSelect, with its value. The row's name: TableSelectionBar shows it, and it names the checkbox "Select <label>". |
| max | number | - | 3 | Set on TableSelectionBar. How many names show before the rest becomes +N. |
| pending | boolean | - | false | Table: the next page, sort or filter is still loading. Change it in a transition (useTransition) so the current rows stay, and pass its isPending: the rows dim to opacity-pending and the table is marked aria-busy instead of going blank. |
| pendingLabel | string | - | Loading | Table: read by screen readers in a polite status while pending. |
Slots
| Name | Required | Description |
|---|---|---|
| toolbar | no | TableToolbar above the table (search, filters, TableActions with buttons) and one below it (the count, TableActions with Pagination). |
| header | no | TableHeader with one TableRow of TableHead cells. |
| body | yes | TableBody with one TableRow per item. |
| caption | no | TableCaption, or aria-label on Table. |
| cells | no | Any component in a TableCell: Avatar with a name, Badge, Checkbox in a fit cell, a DropdownMenu of row actions in a fit cell at the end. |
| empty | no | TableEmpty in TableBody when there are no rows. |
| selection | no | TableSelection around the bar, the toolbars and the table, with TableSelectAll in the header's first fit cell, a TableRowSelect in each row's, and TableSelectionBar above the toolbar. Children of the bar are the app's actions on the picked rows (Export, Delete). |
Anatomy
| Part | Description |
|---|---|
| root | Table: a full-width table, 14px primary text, inside a container that scrolls sideways. |
| header | TableHeader: the header rows. They don't highlight on hover. |
| row | TableRow: a row with a divider under it and the neutral hover overlay. The last body row has no divider. selected fills it with the muted accent and its strong text color. |
| head | TableHead: a header cell, the md control height, 13px medium secondary text on one line. |
| cell | TableCell: a data cell, px-md py-sm, that holds text or any component. align sets start, center or end; numeric sets mono, tabular figures and end; fit shrinks the column to its content. |
| footer | TableFooter: totals on the sunken fill, medium text, a divider above. |
| caption | TableCaption: the name of the table, under it, 13px secondary text. |
| sort | With onSort: a button in the header cell with the header's text and a caret, primary text on hover, a size-target-min hit area. |
| icon | The sort caret: Tabler ChevronUpDown, CaretUp or CaretDown, 16px, secondary. |
| empty | TableEmpty: one row across colSpan columns, 13px secondary text, centered, py-2xl. |
| toolbar | TableToolbar: a row above or below the table, gap-sm, that wraps: a search Input, filters, the count of results. |
| actions | TableActions: the end of a toolbar, gap-sm: buttons that act on the table, or Pagination. |
| selection | TableSelection: a column (gap-md) around the bar, the toolbars and the table. Holds the picked rows and announces their count in a status region. |
| selectAll | TableSelectAll: the Checkbox in the header's fit cell. On when every row on screen is picked, partly on when some are. |
| rowSelect | TableRowSelect: the Checkbox in a row's fit cell, named "Select <label>". A checked one paints its row with the muted accent. |
| selectionBar | TableSelectionBar: shows only while a row is picked. The count, up to max names then +N, Select all, the app's actions, Clear selection. Border, radius-container, surface fill. |
| body | The body rows. With pending they stay on screen and dim to opacity-pending (duration-medium); the container is marked aria-busy, with a polite status next to it. |
Accessibility
Role: table
| Keys | Action |
|---|---|
| Tab | Moves through the sort buttons in the headers and the controls inside cells; the table itself is not focusable. |
| Enter, Space | On a sortable header, sorts by that column. |
- A native table: screen readers read each cell with its column header. TableHead sets scope="col" by default; set scope="row" on a header cell that names its row.
- Name the table with a TableCaption, or aria-label or aria-labelledby when the name is a heading above it.
- A wide table scrolls sideways inside its container instead of widening the page.
- A sortable header is a button inside the header cell; the cell carries aria-sort (ascending or descending) only for the column the table is sorted by.
- Pick rows with a Checkbox in a fit cell, named for its row (Select Ada Lovelace); a header Checkbox picks every row. selected only paints the row: the Checkbox tells screen readers.
- Give an icon-only actions button a name with the row in it: Actions for Ada Lovelace.
- TableEmpty is one row across every column, so the table still has its headers when nothing matches.
- TableSelectAll is a mixed checkbox: aria-checked is mixed while some rows are picked. A press then picks the rest; a press when all are picked clears them.
- TableSelection keeps a status region that reads the count ("2 of 5 rows selected") whenever it changes.
- Select all and Clear selection go away once pressed, so focus moves first: to Clear selection after Select all, to the header checkbox after Clear selection.
Install
npx shadcn add https://werewolf-ui.vercel.app/r/table.jsonThis also adds styles/werewolf.css, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after @import "tailwindcss", with a path relative to that CSS file (from app/globals.css it is @import "../styles/werewolf.css").