Skip to content

Customize AdaptTable — slots, classNames, headless prop-getters

See it working: the unstyled adapter in the live demo — same engine, your own classes.

A spectrum, all opt-in: restyle parts with classNames, replace parts with slots, tune the chrome with props, or theme through your kit’s provider.

Restyle without replacing. Mantine and Chakra expose five wrapper hooks (root, toolbar, table, card, footer); the unstyled adapter exposes a hook for every rendered node:

import { DataTable } from "@adapttable/unstyled";
<DataTable
data={data}
columns={columns}
rowKey={(r) => r.id}
classNames={{
table: "w-full text-sm",
row: "border-b hover:bg-zinc-50 data-[selected]:bg-blue-50",
cell: "px-3 py-2",
filtersPopover: "rounded-lg border bg-white shadow-xl",
filtersDone: "rounded-md bg-zinc-900 text-white px-3 py-2",
}}
/>;

Every unstyled node also carries a stable data-adapttable-part attribute — the kebab-case of the classNames key (searchFielddata-adapttable-part="search-field") — plus data-* state attributes, so plain CSS, Tailwind, and shadcn tokens all work. The full part map:

Using shadcn/ui? @adapttable/shadcn is this same unstyled adapter with the shadcn class preset already applied — import DataTable from @adapttable/shadcn and pass classNames to override only the parts you name.

PartElement
rootThe outer wrapper around the whole table.
toolbarThe toolbar row (search, filters, columns, views, your toolbar).
searchFieldThe search field wrapper (input + leading icon).
searchThe search <input>.
searchIconThe leading magnifying-glass icon.
sortSelectThe mobile sort-by <select>.
rowsPerPageRows-per-page <select> (toolbar in infinite mode, footer when paged).
PartElement
filtersButtonThe Filters trigger button.
filtersIconThe funnel icon inside the trigger.
filtersCountThe active-filter count badge.
exportCsvButtonThe Export CSV toolbar button (exportCsv prop).
filtersAnchorThe popover anchor wrapper around the trigger.
filtersPopoverThe anchored popover card (filtersMode="popover").
filtersBackdropThe drawer backdrop (filtersMode="drawer").
filtersPanelThe drawer panel.
filtersHeader / filtersTitle / filtersClosePanel header, its title, and the close button.
filtersBody / filtersFooterThe panel content area and its action row.
filtersClear / filtersDoneThe clear-all and done/apply buttons.
filterField / filterLabelOne auto-built field’s wrapper and its caption.
filterInput / filterSelect / filterOperatorText/date/number inputs, the select widget, and a range field’s operator <select>.
filterCheckboxGroup / filterCheckboxA multiSelect checkbox list and one option.
filterOptionsLoadingThe placeholder shown while async options load.
PartElement
chipsThe active-filter chip strip.
chipOne removable chip.
chipRemoveA chip’s remove button.
PartElement
columnMenu / columnMenuButton / columnMenuPanelThe menu wrapper, trigger, and dropdown panel.
columnMenuHeader / columnMenuTitleThe panel header and its title.
columnMenuItem / columnMenuGrip / columnMenuLabelOne column row, its drag grip, and its label.
columnMenuVisibility / columnMenuPinThe show/hide toggle and the pin control.
columnMenuSeparator / columnMenuResetThe separator above the actions entry and the reset button.
resizeHandleA header’s drag/keyboard resize handle.
PartElement
viewsButton / viewsPanelThe menu trigger and dropdown panel.
viewsItem / viewsDeleteOne view’s apply button and its delete button.
viewsInput / viewsSaveThe view-name input and the save button.
PartElement
bulkBar / bulkButtonThe selection toolbar and one bulk-action button.
selectAllBanner / selectAllText / selectAllButtonThe cross-page banner, its status text, and its action.
selectionCell / checkboxA row’s selection cell and the checkbox itself.
PartElement
table / thead / tbodyThe <table> and its sections.
headerRow / headerCellThe header <tr> and one <th>.
groupRow / groupCellThe grouped-header row and one spanning cell.
sortButton / sortIndexA sortable header’s button and its multi-sort badge.
row / cellOne body <tr> and one <td>.
actionsCell / actionButtonThe trailing actions cell and one action button.
PartElement
expandHeader / expandCellThe leading chevron header cell and body cell.
expandButtonThe expand/collapse chevron (rows and cards).
detailRow / detailCellThe full-width detail <tr> and its spanning <td>.
cardDetailThe detail section inside an expanded mobile card.

Opt-in via onCellEdit — see Inline cell editing. When editing is dormant these parts are never mounted.

PartElement
edit-cell-activateInvisible activate control (double-click / Enter / F2 begins edit).
edit-cell-editorKit-native input / select while the cell is active.

Opt-in via groupBy — see Row grouping. When grouping is dormant these parts are never mounted.

PartElement
group-rowDesktop group header <tr> (or antd’s grouped row wrapper).
group-cellThe spanning <td> / <th> inside a group header (most kits).
group-cardGroup header block in the mobile card list.
group-toggleExpand / collapse chevron (aria-expanded, expandGroup labels).
group-labelThe group’s display label (bucket value).
group-countLeaf count beside the label (labels.groupCount).
group-selectTri-state checkbox over the group’s leaf rows (when selection is on).
group-aggregateOne per-group aggregate cell (data-column = column key).
PartElement
cards / cardThe card list and one card.
cardRow / cardLabel / cardValueOne label/value line inside a card.
summary / summaryRow / summaryCellThe <tfoot>, its <tr>, and one summary <td>.
summaryCardThe trailing summary card in the mobile list.
PartElement
footer / pagerThe footer bar and the trailing pager group (page-of label + numbered pages).
pageButton / pageEllipsisEvery pager button (prev/next + each numbered page; the current carries aria-current="page") and the ”…” elision gap.
loadMore / loadMoreButtonThe infinite-mode sentinel area and its button.
empty / emptyClearThe empty state and its clear-filters button.
loading / refreshIndicatorThe first-load skeleton and the background-refresh bar.
error / retryButtonThe error state and its retry button.

A few purely structural nodes (scroll-box, virtual-spacer, the skeleton internals) expose only the data-adapttable-part attribute.

Replace whole sub-components on any adapter:

<DataTable
data={data}
columns={columns}
rowKey={(r) => r.id}
slots={{ empty: <MyEmptyState />, skeleton: <MySkeleton /> }}
/>

skeleton replaces the first-load skeleton; empty replaces the empty-state. The error state is built-in (retry button included) — translate it via the errorTitle / errorMessage / retry labels.

<DataTable
data={data}
columns={columns}
rowKey={(r) => r.id}
density="compact"
/>

"comfortable" (default) is the roomy layout; "compact" tightens row height and padding. Each adapter maps it to its kit’s table size — MUI "comfortable"medium / "compact"small, antd → middle / small, Radix "2" / "1" — and MUI, Chakra, antd, and Radix offer an explicit size prop that overrides the mapping (e.g. antd size="large").

Opt in with exportCsv to render a kit-native Export CSV button next to Filters / Columns. The file mirrors the current view’s data — the active search, filters, and sort — with the full exportable column set in display order, regardless of viewport: the same button produces the same file on phone and desktop (hideOnMobile never shrinks an export).

Cells that a spreadsheet would execute as formulas (=, +, -, @, tab or carriage-return prefixes) are neutralised with a leading ' by default; pass escapeFormulas: false in the options object if you need raw output for a non-spreadsheet pipeline.

<DataTable
data={people}
columns={columns}
rowKey={(r) => r.id}
exportCsv // defaults: export.csv, current page
/>
<DataTable
data={people}
columns={columns}
rowKey={(r) => r.id}
exportCsv={{ filename: "people.csv", scope: "all" }}
/>
  • scope: "page" (default) — the current page / loaded slice.
  • scope: "all" — the full filtered+sorted set when the source exposes it (frontend does); server-backed sources fall back to the current page unless you wire your own download against an export endpoint via toolbar.

Headless helpers remain available: rowsToCsv, downloadCsv, and downloadTableCsv from @adapttable/core.

The export path is exported end to end: exportableColumns filters the visible layout to columns with exportable values, buildTableCsv turns rows + columns into CSV text (RowsToCsvOptions controls delimiter, BOM and escapeFormulas), resolveExportCsv normalizes the exportCsv prop (ExportCsvOptions), and makeExportCsvHandler wires all of it to a download handler the toolbar button calls. Custom toolbars can reuse any stage.

<DataTable
data={data}
columns={columns}
rowKey={(r) => r.id}
stickyHeader // keep the desktop header pinned while the page scrolls
stickyTop={56} // offset under your app header (also offsets the toolbar)
maxHeight={420} // fixed-height scroll box instead of page scroll
/>

maxHeight turns the table into a scroll box that also scrolls sideways — the header and pinned columns stick within it, which is what makes column pinning visibly stick. scrollToTopOnChange (default true) scrolls back to the table when search/filter/page changes, with scrollTopGap (default 8) of breathing room below sticky chrome.

The core is style-free: wrap your app in the kit’s provider and AdaptTable renders with that kit’s real components, following its theme and dark mode (prefers-color-scheme) automatically.

animate works on every adapter — a dependency-free row/card entrance stagger that honours reduced motion. Rolling your own? Every animatable row/card carries a data-stagger attribute, so leave animate off and target those elements with GSAP/Framer Motion.

Kit-specific knobs:

  • MUIsize ("small" | "medium") overrides the density mapping; className lands on the root <Paper>.
  • ChakraaccentColor colors primary accents (buttons, badges); size ("sm" | "md" | "lg", default "md").
  • Ant Designsize ("small" | "middle" | "large"), bordered for cell borders, className on the wrapper. The virtualized scroll area is bounded by the shared maxHeight prop.
  • Unstyled — no provider needed; theme entirely through classNames, the data-adapttable-part hooks above, and your own CSS variables / data-theme.

Pass a Cell component (receives { row, rowIndex }; define it at module level so its identity is stable) or a lighter accessor:

import type { CellProps } from "@adapttable/core";
function StatusCell({ row }: CellProps<Person>) {
return (
<Badge color={row.status === "active" ? "green" : "gray"}>
{row.status}
</Badge>
);
}
const columns = [
{ key: "name", sortable: true },
{ key: "status", Cell: StatusCell },
{
key: "salary",
accessor: (r: Person) => formatMoney(r.salary),
align: "end",
},
];

See the Columns guide and the ColumnDef table for the full column surface (sortValue, i18n, group, hideOnMobile, …).