// preview in
saved for every page

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.

Built on TanStack TablePattern WAI-ARIA tableClient componentSource ↗
minimal · light · #3fa34d
#4821Padaria Estrela$1,240.00
#4820Vila Nova Design$840.50
#4819Aurora BioTech$3,120.00
0 of 3 selected
selectable
loading

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.

minimal · light · #3fa34d
#4821Padaria EstrelaPaid$1,240.00
#4820Vila Nova DesignPending$840.50
#4819Aurora BioTechPaid$3,120.00
Order
#4819Aurora BioTech10/19/2026$3,120.00
#4821Padaria Estrela10/24/2026$1,240.00
#4820Vila Nova Design10/22/2026$840.50
#4818Café Serra Azul10/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.

#4821Padaria Estrela$1,240.00
#4820Vila Nova Design$840.50
#4819Aurora BioTech$3,120.00
1 of 3 selected

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".

#4821Padaria Estrela$1,240.00
#4820Vila Nova Design$840.50
#4819Aurora 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.

#4821Padaria Estrela$1,240.00
#4820Vila Nova Design$840.50
#4819Aurora 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 yet

Orders 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.

#4821Padaria Estrela$1,240.00
#4820Vila Nova Design$840.50
#4819Aurora BioTech$3,120.00
#4818Café Serra Azul$420.00
#4817Livraria 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.

Actions
#4821Padaria Estrela$1,240.00
#4820Vila Nova Design$840.50
#4819Aurora 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

#4821Padaria EstrelaPaid$1,240.00
#4820Vila Nova DesignPaid$840.50
#4819Aurora BioTechShipped$3,120.00
#4818Café Serra AzulPaid$420.00
0 of 4 selected
Orders to shipDataTable · Button · Badge
Aline PradoFrontend architectActive
Tiago CostaDevOps engineerActive
Helena LimaProduct managerInvited
Marcos SilvaSecurity analystSuspended
1 member selected
Team membersDataTable · Avatar · Badge
Description
10/24/2026Retainer, Vila Nova DesignRevenue+$4,800.00
10/22/2026Cloud hostingInfrastructure-$920.40
10/20/2026Design tool licensesSoftware-$145.00
10/18/2026Milestone 2, Aurora BioTechRevenue+$2,200.00
Account activityDataTable · Badge

Guidelines

ProductRevenue
Annual plan$142,850.00
Monthly plan$38,400.50
Add-on storage$2,190.00
DoAlign numbers to the right.

Set meta: { align: "end" } on number columns. The digits get the same width, so the biggest value stands out.

ProductRevenue
Annual plan$142,850.00
Monthly plan$38,400.50
Add-on storage$2,190.00
Don’tNumbers aligned to the left.

Thousands and hundreds start at the same place, and people have to read every value to compare them.

EmailPhone
Padaria Estrelacompras@estrela.com.br(11) 3456-7821
Café Serra Azulpedidos@serraazul.com.br(31) 2233-4410
DoSort only the columns people sort by.

Here only the name sorts. Fewer buttons in the header means fewer Tab stops before the data.

Padaria Estrelacompras@estrela.com.br(11) 3456-7821
Café Serra Azulpedidos@serraazul.com.br(31) 2233-4410
Don’tA sort button on every column.

Every column with an accessor sorts by default. Nobody sorts phone numbers, and each extra button is one more stop for keyboard users.

Props

PropTypeDefaultDescription
data*TData[]—Array of row data records to display.
columns*ColumnDef<TData, any>[]—TanStack Table column definitions. Columns with an accessor are sortable unless they set enableSorting: false; meta: { align: "end" } right-aligns numbers.
captionstring"Data table"Names the table (aria-label) and, while it overflows, its scroll area. Say what the rows are.
getRowId((originalRow: TData, index: number) => string)—Returns a stable id per row. Selection state is keyed by it; without it, rows are keyed by index.
selectablebooleanfalseEnable row checkboxes and batch selection.
rowSelectionRowSelectionState—Controlled row selection state map.
defaultRowSelectionRowSelectionState{}Uncontrolled initial row selection state map.
onRowSelectionChangeOnChangeFn<RowSelectionState>—Callback fired when row selection state changes.
sortingSortingState—Controlled column sorting state.
defaultSortingSortingState[]Uncontrolled initial column sorting state.
onSortingChangeOnChangeFn<SortingState>—Callback fired when column sorting state changes.
pageSizenumber—Rows per page. Without it, every row shows on one page.
pageCountnumber—Explicit total number of pages for server-side pagination.
pagenumber—Controlled current page, starting at 1. Without it, the table keeps the page itself.
onPageChange((page: number) => void)—Callback fired when the active page index changes.
loadingbooleanfalseRender placeholder skeleton rows during asynchronous fetching.
loadingRowsnumber4Number of skeleton placeholder rows to render when loading.
emptyStateReactNode"No records found."Content displayed when data is empty and not loading.
selectAllLabelstring"Select all rows"Accessible label for the header select-all checkbox.
selectRowLabel((row: TData, index: number) => string)"Select row {n}"Accessible name of each row checkbox. Include something that identifies the row. The index counts across all pages, in data order.
selectionStatus((selectedCount: number, totalCount: number) => ReactNode)"{selected} of {total} selected"Text of the footer status, from the selected and total counts. Shown only when selectable.
totalRowsnumber—Total count of dataset records for server-side pagination summaries.
paginationLabelstring"Table pagination"Accessible navigation label for the pagination control.
previousLabelReactNode"Previous"Label or icon for the previous page navigation button.
nextLabelReactNode"Next"Label or icon for the next page navigation button.
pageLabel((page: number) => string)"Page {n}"Accessible name of each page button.

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

data-alignstart · end, on heads and cells
data-sortedasc · desc, on the sort button
data-selectedon a selected row
data-selection-cellon the checkbox head and cells
data-loadingon skeleton rows
data-emptyon the empty row (data-empty-cell on its cell)

Tokens it reads

Color

--cml-accent--cml-accent-text--cml-border--cml-border-control--cml-card-border--cml-card-shadow--cml-fg--cml-fg-secondary--cml-focus-offset--cml-focus-ring--cml-focus-width--cml-surface-raised--cml-table-head-fg--cml-table-row-selected-fg

Shape & size

--cml-border-width--cml-radius-control-sm--cml-radius-popover

Type & motion

--cml-font-label

This component only

--cml-corner-shape--cml-table-head-tracking--cml-table-head-transform

Accessibility

TabMoves through the sort buttons, the checkboxes, the controls in cells and the page buttons. When the table is too wide, the scroll area is one extra stop.
Enter / SpaceOn a sort button, sorts by that column, then reverses it, then clears it. On a page button, goes to that page.
SpaceChecks or unchecks the focused row checkbox, or every row from the header checkbox.
← / →Scroll a wide table sideways while its scroll area has focus.
Was this page helpful? · Edit this page on GitHub