Every React project reaches for a data table eventually, and the choice always feels like a trap:
- Reach for ag-Grid or MUI X DataGrid and you get power — but their look, their DOM, and (for the good features) their license.
- Reach for TanStack Table and you get total freedom — but now you build every cell, header, filter, and pagination control from scratch.
- Reach for a kit-specific table (Mantine, Chakra, Ant) and you're locked to that one kit, re-learning a new API each time.
I wanted one table that renders natively in whatever UI kit a project already uses, with the same features everywhere, and a headless escape hatch when the defaults aren't enough. So I built AdaptTable.
One API, real native components
The whole table is five lines:
import { DataTable, useFrontendData, type ColumnDef } from "@adapttable/mantine";
const columns: ColumnDef<Person>[] = [
{ key: "name", header: "Name", accessor: (r) => r.name, sortable: true },
{ key: "email", header: "Email", accessor: (r) => r.email },
];
function People({ rows }: { rows: Person[] }) {
const source = useFrontendData({ data: rows, columns });
return <DataTable source={source} columns={columns} rowKey={(r) => r.id} />;
}
Swap the import for @adapttable/mui, @adapttable/chakra, @adapttable/antd, @adapttable/radix, @adapttable/shadcn, or @adapttable/unstyled — same props, different kit.
The key part: it is not a wrapper that looks the same everywhere. Each adapter renders that kit's actual components — MUI gets Material elevation, Ant gets Ant's compact density, Chakra gets Chakra's tokens. Same behavior, native skin. Flip the switcher in the live demo and watch one dataset re-render through each kit: https://orwa-mahmoud.github.io/adapttable/demo/
Filters and state that live in the URL
My favorite part: open the filter drawer, set a status and a date range, copy the link — whoever opens it sees the exact same view. Search, filters, sort, and page all serialize into query params (q, page, f_status, …), so every view is refresh-safe, back/forward-safe, and shareable. No useState juggling, no wiring: declare the filters and the URL sync comes with them, identically for client-side data and server-side fetching (there's a router adapter for Next.js and react-router).
What you get for free
Write it once, get all of it across every adapter:
- Sorting, filtering (with removable chips), global search
- Row selection + bulk actions
- Column management — show/hide, reorder, pin (sticky), resize
- Pagination — numbered or infinite scroll, auto by device
- Shareable URL state + saved views
- First-class RTL / Arabic and dark mode
- Optional virtualization — a 10k-row list mounts ~24 DOM rows
- Client-side data and server-side (you supply the fetch) through one contract
Headless when you need it
Under every adapter is @adapttable/core — a headless engine with prop-getters (getTableProps, getRowProps, …). If the batteries-included adapter isn't enough, drop down and render your own markup on the same engine. You never hit a wall.
What it's not
No pivoting, grouping, or Excel-style editing — this is for the CRUD tables most apps ship, not analytics grids. If you need those, ag-Grid is the right tool.
Try it
- Try it in your browser (no install): https://stackblitz.com/github/orwa-mahmoud/adapttable/tree/main/starters/mantine
- Live demo (flip the kit switcher): https://orwa-mahmoud.github.io/adapttable/demo/
- Docs: https://orwa-mahmoud.github.io/adapttable/
npx @adapttable/cli init- MIT, stable v1 (1.1 just brought every adapter to full feature parity): https://github.com/orwa-mahmoud/adapttable
Feedback very welcome — especially whether the per-kit rendering holds up in your stack, and how the declarative API feels next to what you use today.
Top comments (0)