DataTable
A table driven by data and column definitions, with sorting, row selection, pages and a loading state built in. For a static table you write by hand, use Table.
| #4821 | Padaria Estrela | $1,240.00 | |
| #4820 | Vila Nova Design | $840.50 | |
| #4819 | Aurora BioTech | $3,120.00 |
Installation
Set up Camaleo once (Installation), then import the table and the column type. It runs on the client.
import { DataTable, type ColumnDef } from "@camaleo/react";Anatomy
A native <table> inside a container that scrolls sideways when the table is too wide, with a footer for the status and the pages. The footer only shows when the table is selectable or has more than one page.
Examples
Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above.
| #4821 | Padaria Estrela | Paid | $1,240.00 |
| #4820 | Vila Nova Design | Pending | $840.50 |
| #4819 | Aurora BioTech | Paid | $3,120.00 |
| Order | |||
|---|---|---|---|
| #4819 | Aurora BioTech | 10/19/2026 | $3,120.00 |
| #4821 | Padaria Estrela | 10/24/2026 | $1,240.00 |
| #4820 | Vila Nova Design | 10/22/2026 | $840.50 |
| #4818 | Café Serra Azul | 10/15/2026 | $420.00 |
Sorting
Click a header to sort. Numbers start with the biggest, text starts at A, and a third click goes back to the original order. Turn sorting off with enableSorting: false where the order means nothing, like an id.
| #4821 | Padaria Estrela | $1,240.00 | |
| #4820 | Vila Nova Design | $840.50 | |
| #4819 | Aurora BioTech | $3,120.00 |
Row selection
Use selectable when people act on several rows at once. Name each checkbox after its row with selectRowLabel, so a screen reader says which order it selects, not "Select row 2".
| #4821 | Padaria Estrela | $1,240.00 |
| #4820 | Vila Nova Design | $840.50 |
| #4819 | Aurora BioTech | $3,120.00 |
Pages
Past a screenful of rows, set pageSize. Say which rows are showing in selectionStatus, and translate the page buttons with previousLabel, nextLabel and pageLabel.
| #4821 | Padaria Estrela | $1,240.00 |
| #4820 | Vila Nova Design | $840.50 |
| #4819 | Aurora BioTech | $3,120.00 |
Loading
Click Refresh. loading swaps the rows for skeletons, but skeletons say nothing to a screen reader, so a status message announces when loading starts and ends.
No orders yetOrders show up here as soon as a customer pays at checkout. | ||
No rows yet
Pass an EmptyState to emptyState. The headers stay, so people see what the table will hold, and the message gives them the next step.
Customer name or order number.
| #4821 | Padaria Estrela | $1,240.00 |
| #4820 | Vila Nova Design | $840.50 |
| #4819 | Aurora BioTech | $3,120.00 |
| #4818 | Café Serra Azul | $420.00 |
| #4817 | Livraria Ponto Final | $2,890.00 |
Search
Type a customer name. The table filters what you pass to data, a status line announces how many orders match, and Clear search sends focus back to the field.
| #4821 | Padaria Estrela | $1,240.00 | |
| #4820 | Vila Nova Design | $840.50 | |
| #4819 | Aurora BioTech | $3,120.00 |
Actions in a row
Icon buttons get a name with the row, like "Download invoice for #4821", because "Download" alone is the same in every row. The column header stays for screen readers with VisuallyHidden.
In context
A data table is usually the main piece of an admin page, with actions above it. These are whole pieces of UI you can copy, in the skin you picked.
Orders to ship
| #4821 | Padaria Estrela | Paid | $1,240.00 | |
| #4820 | Vila Nova Design | Paid | $840.50 | |
| #4819 | Aurora BioTech | Shipped | $3,120.00 | |
| #4818 | Café Serra Azul | Paid | $420.00 |
| Aline Prado | Frontend architect | Active | |
| Tiago Costa | DevOps engineer | Active | |
| Helena Lima | Product manager | Invited | |
| Marcos Silva | Security analyst | Suspended |
| Description | |||
|---|---|---|---|
| 10/24/2026 | Retainer, Vila Nova Design | Revenue | +$4,800.00 |
| 10/22/2026 | Cloud hosting | Infrastructure | -$920.40 |
| 10/20/2026 | Design tool licenses | Software | -$145.00 |
| 10/18/2026 | Milestone 2, Aurora BioTech | Revenue | +$2,200.00 |
Guidelines
| Product | Revenue |
|---|---|
| Annual plan | $142,850.00 |
| Monthly plan | $38,400.50 |
| Add-on storage | $2,190.00 |
Set meta: { align: "end" } on number columns. The digits get the same width, so the biggest value stands out.
| Product | Revenue |
|---|---|
| Annual plan | $142,850.00 |
| Monthly plan | $38,400.50 |
| Add-on storage | $2,190.00 |
Thousands and hundreds start at the same place, and people have to read every value to compare them.
| Phone | ||
|---|---|---|
| Padaria Estrela | compras@estrela.com.br | (11) 3456-7821 |
| Café Serra Azul | pedidos@serraazul.com.br | (31) 2233-4410 |
Here only the name sorts. Fewer buttons in the header means fewer Tab stops before the data.
| Padaria Estrela | compras@estrela.com.br | (11) 3456-7821 |
| Café Serra Azul | pedidos@serraazul.com.br | (31) 2233-4410 |
Every column with an accessor sorts by default. Nobody sorts phone numbers, and each extra button is one more stop for keyboard users.
Props
enableSorting: false; meta: { align: "end" } right-aligns numbers.aria-label) and, while it overflows, its scroll area. Say what the rows are.selectable.It also takes the attributes of a <div>, like aria-busy and style. Columns are TanStack ColumnDefs: accessorKey or accessorFn, header, cell, enableSorting and meta.align.
Styling hooks
Restyle it without forking: target the attributes, or set the tokens on a parent, a skin or the table itself.
Attributes
Tokens it reads
Color
Shape & size
Type & motion
This component only
Accessibility
- The caption prop names the table. Say what the rows are, like "Orders in October 2026"; the default "Data table" tells people nothing.
- Sortable headers get aria-sort (ascending, descending or none), so screen readers say how the rows are sorted.
- The header checkbox turns indeterminate when only some rows are selected, and selected rows get aria-selected.
- Skeleton rows are silent. Add a role="status" message for loading, and for result counts after a search.