DEV Community

Cover image for One headless table engine, every React UI kit
Orwa Mahmoud
Orwa Mahmoud

Posted on

One headless table engine, every React UI kit

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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

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)