A production-ready React DataTable built on AG Grid Community that ships enterprise-grade features out of the box — searchable set filters, numeric filters, column header menus, integrated pagination, server-side data sourcing, and a classical theme with dark mode.
Built with Radix UI primitives (shadcn-style) and Tailwind CSS.
- Searchable Set Filter — Checkbox list filter with search input, Select All, and a scrollable value list
- Number Filter — Operator dropdown with Equals, Not equal, Greater than, Less than, Between, Blank, and more
- Column Header Menu — Sort, pin, autosize, filter access, column chooser, and reset — all from the header
- Integrated Pagination — Works with both local data and API mode; filter-aware page counts
- API Mode (DataSource) — Pass an async function, and the table handles loading, error, and empty states
- CSV / Excel Export — One-click download as CSV or Excel with an Export button; respects visible columns and active filters
- Loading / Error / Empty Overlays — Built-in skeleton loader, error with retry, and empty state — all overridable
- Classical Theme — CSS-variable-based theme (
--cdt-*prefix) with dark mode support - Two Modes — Local mode with client-side filtering/sorting, or API mode with server-side queries
- TypeScript First — Full generic type support (
DataTable<TData>) and exported utility types
npm install @sandylib27/react-ag-datatable ag-grid-community ag-grid-react| Package | Version |
|---|---|
react |
>=18 |
react-dom |
>=18 |
ag-grid-community |
>=35 |
ag-grid-react |
>=35 |
Import the stylesheet once in your app entry point:
import '@sandylib27/react-ag-datatable/styles.css';Note: AG Grid v35+ uses the Theming API. You do not need to import
ag-grid.cssorag-theme-quartz.css— the theme is applied programmatically viathemeQuartz.withParams()inside the component.
import { DataTable } from '@sandylib27/react-ag-datatable';
import '@sandylib27/react-ag-datatable/styles.css';
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', age: 28 },
{ id: 2, name: 'Bob', email: 'bob@example.com', age: 34 },
{ id: 3, name: 'Carol', email: 'carol@example.com', age: 42 },
];
const columns = [
{ field: 'name', headerName: 'Name' },
{ field: 'email', headerName: 'Email' },
{ field: 'age', headerName: 'Age', type: 'numericColumn' },
];
export function UsersTable() {
return (
<DataTable
data={users}
columns={columns}
bordered
hoverable
enableSorting
enableColumnFilter
pagination
/>
);
}Pass a dataSource function instead of data. The component builds a DataTableQuery object and calls your function whenever the page, sort, or filters change.
import {
DataTable,
type DataTableQuery,
type DataTableResponse,
type SetFilterValuesSource,
} from '@sandylib27/react-ag-datatable';
interface User {
id: number;
name: string;
email: string;
role: string;
}
async function fetchUsers(query: DataTableQuery): Promise<DataTableResponse<User>> {
const params = new URLSearchParams({
page: String(query.page),
pageSize: String(query.pageSize),
...(query.sort ? { sortField: query.sort.field, sortDir: query.sort.direction } : {}),
});
const res = await fetch(`/api/users?${params}`);
return res.json(); // { rows: User[], totalRows: number }
}
const fetchSetFilterValues: SetFilterValuesSource = async ({ field, filters }) => {
const res = await fetch('/api/users/filter-values', {
method: 'POST',
body: JSON.stringify({ field, filters }),
headers: { 'Content-Type': 'application/json' },
});
return res.json(); // e.g. ["Admin", "Editor", "Viewer"]
};
export function UsersTable() {
return (
<DataTable<User>
dataSource={fetchUsers}
setFilterValuesSource={fetchSetFilterValues}
columns={[
{ field: 'name', headerName: 'Name' },
{ field: 'email', headerName: 'Email' },
{ field: 'role', headerName: 'Role' },
]}
bordered
hoverable
enableSorting
enableColumnFilter
enableColumnMenu
pagination={{ pageSize: 25, pageSizeOptions: [25, 50, 100] }}
/>
);
}When API mode only loads one page at a time, set filters should not derive their
checkbox list from the current grid rows. Provide setFilterValuesSource so the
filter can fetch distinct values from the server. The table passes the target
field plus the other active filters, excluding the current column's own filter.
The DataTableQuery sent to your function looks like:
{
page: 0,
pageSize: 25,
sort: { field: 'name', direction: 'asc' } | null,
filters: {
role: { type: 'set', values: ['Admin', 'Editor'] },
age: { type: 'number', operator: 'greaterThan', value: 18 },
}
}Enabled by default on text columns when enableColumnFilter is set. Renders a popover with a search input, Select All toggle, and a scrollable checkbox list of unique values.
<DataTable data={data} columns={columns} enableColumnFilter />In API mode, pass setFilterValuesSource to supply the complete value list for
each set-filtered column. Without it, the filter falls back to scanning the rows
currently loaded in the grid, which is only complete in local mode.
Automatically applied to columns with type: 'numericColumn'. Provides an operator dropdown and value input(s).
const columns = [
{ field: 'price', headerName: 'Price', type: 'numericColumn' },
];Available operators: equals, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, between, blank, notBlank.
A dropdown menu on each column header with sort controls, pinning options, autosize, filter access, and column visibility.
<DataTable
data={data}
columns={columns}
enableColumnMenu
enableSorting
enableColumnFilter
/>Menu actions:
- Sort Ascending / Descending / Clear Sort
- Pin Left / Pin Right / Unpin
- Autosize This Column / Autosize All Columns
- Open Filter
- Choose Columns
- Reset Columns
A standalone panel that lets users toggle column visibility.
<DataTable data={data} columns={columns} enableColumnVisibility />Drag-to-reorder rows. The reordered dataset is emitted via onRowReorder.
<DataTable
data={items}
columns={columns}
enableRowReorder
onRowReorder={(reorderedItems) => setItems(reorderedItems)}
/>Enable with a boolean for defaults, or pass a config object.
// Default: pageSize 15, options [15, 25, 50, 100]
<DataTable data={data} columns={columns} pagination />
// Custom
<DataTable
data={data}
columns={columns}
pagination={{ pageSize: 25, pageSizeOptions: [25, 50, 100] }}
/>
// Shorthand props
<DataTable data={data} columns={columns} pagination pageSize={50} />Enable a toolbar Export button that downloads the visible, filtered data as CSV or Excel.
<DataTable
data={data}
columns={columns}
enableExport
exportFileName="products"
/>The export respects:
- Visible columns — hidden columns are excluded
- Active filters — only filtered rows are exported
- Value formatters — formatted values (e.g.
$1,234) are used in the output
You can also call the export functions directly for custom triggers:
import { exportToCSV, exportToExcel } from '@sandylib27/react-ag-datatable';
// With a GridApi reference
exportToCSV(gridApi, { fileName: 'report.csv' });
exportToExcel(gridApi, { fileName: 'report.xlsx' });Override the built-in loading, error, and empty state overlays.
<DataTable
dataSource={fetchData}
columns={columns}
loadingOverlay={<MyLoadingSpinner />}
errorOverlay={(error, retry) => (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={retry}>Retry</button>
</div>
)}
emptyOverlay={<p>No records found.</p>}
/><DataTable
data={data}
columns={columns}
hoverable
onRowClick={(row) => navigate(`/users/${row.id}`)}
rowClassName={(row) => (row.isArchived ? 'opacity-50' : undefined)}
/>Rows pinned to the top are always visible and unaffected by sort or filter.
<DataTable
data={data}
columns={columns}
pinnedTopRows={[{ id: 'total', name: 'Total', amount: 1250 }]}
/>Control which rows participate in filtering. Rows that fail the predicate are always displayed.
<DataTable
data={data}
columns={columns}
enableColumnFilter
isRowFilterable={(row) => !row.isPinned}
/>The component uses CSS variables with a --cdt- prefix. Override them on any ancestor to customize the look.
/* Light theme (default) */
[data-cdt-datatable] {
--cdt-background: 0 0% 100%;
--cdt-foreground: 222.2 84% 4.9%;
--cdt-muted: 210 40% 96.1%;
--cdt-muted-foreground: 215.4 16.3% 46.9%;
--cdt-border: 214.3 31.8% 91.4%;
--cdt-ring: 222.2 84% 4.9%;
--cdt-primary: 222.2 47.4% 11.2%;
--cdt-primary-foreground: 210 40% 98%;
--cdt-destructive: 0 84.2% 60.2%;
--cdt-hover: 210 40% 96.1%;
--cdt-header-bg: 210 40% 98%;
--cdt-row-hover: 210 40% 96.1%;
--cdt-row-selected: 214.3 31.8% 91.4%;
--cdt-skeleton: 210 40% 93%;
--cdt-radius: 0.5rem;
--cdt-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
/* Dark mode */
.dark [data-cdt-datatable],
[data-cdt-datatable].dark {
--cdt-background: 222.2 84% 4.9%;
--cdt-foreground: 210 40% 98%;
--cdt-muted: 217.2 32.6% 17.5%;
--cdt-muted-foreground: 215 20.2% 65.1%;
--cdt-border: 217.2 32.6% 17.5%;
--cdt-header-bg: 222.2 47.4% 8.2%;
--cdt-row-hover: 217.2 32.6% 12%;
--cdt-row-selected: 217.2 32.6% 17.5%;
--cdt-skeleton: 217.2 32.6% 14%;
}CSS variable values use the HSL channel format (H S% L% without the hsl() wrapper) so they work with Tailwind's opacity modifier syntax.
The repo includes 8 interactive examples you can run in a browser during development:
git clone https://github.com/sandylib27/react-ag-datatable.git
cd react-ag-datatable
npm install
npm run dev:examplesOpens at http://localhost:5173 with a sidebar listing all examples:
| Example | What it shows |
|---|---|
| Basic | Minimal table with sorting and row click |
| Set Filter | Searchable checkbox filter on text columns |
| Number Filter | Operator dropdown filter on numeric columns |
| Column Menu | Header menu with sort, pin, autosize, column chooser |
| Local Pagination | Client-side pagination with page size selector |
| API Connected | Server-side data fetching with loading/error states |
| API Set Values | Server-side pagination with complete set-filter values |
| Export | CSV and Excel download with filtered data |
| Theming | CSS variable overrides and dark mode toggle |
| Full Featured | All features combined in a single table |
The example app resolves imports to the local src/ so changes are reflected instantly via Vite HMR. It is not included in the published package.
| Prop | Type | Default | Description |
|---|---|---|---|
data |
TData[] |
— | Row data for local mode. Mutually exclusive with dataSource. |
dataSource |
DataTableDataSource<TData> |
— | Async data fetcher for API mode. Mutually exclusive with data. |
setFilterValuesSource |
SetFilterValuesSource |
— | Complete set-filter values for API mode. Falls back to scanning loaded rows when omitted. |
columns |
ColDef<TData>[] |
— | AG Grid column definitions. Required. |
size |
'sm' | 'md' |
'md' |
Row density. 'sm' = 36 px rows, 'md' = 44 px rows. |
bordered |
boolean |
false |
Show outer border and rounded corners. |
hoverable |
boolean |
false |
Highlight rows on hover. |
enableSorting |
boolean |
false |
Enable click-to-sort on column headers. |
enableColumnFilter |
boolean |
false |
Enable column filter panels (SetFilter for text, NumberFilter for numeric). |
enableColumnMenu |
boolean |
false |
Enable the column header dropdown menu. |
enableColumnReorder |
boolean |
false |
Enable drag-to-reorder columns. |
enableRowReorder |
boolean |
false |
Enable drag-to-reorder rows. |
enableColumnVisibility |
boolean |
false |
Show the column visibility toggle panel. |
enableExport |
boolean |
false |
Show the CSV/Excel export button in the toolbar. |
exportFileName |
string |
'export' |
File name for exports (without extension). |
pagination |
boolean | PaginationConfig |
false |
Enable pagination. Pass true for defaults or a config object. |
pageSize |
number |
15 |
Default page size. Shorthand for pagination={{ pageSize }}. |
pageSizeOptions |
number[] |
[15, 25, 50, 100] |
Available page sizes in the dropdown. |
onRowReorder |
(data: TData[]) => void |
— | Callback with the reordered dataset after a row drag. |
rowIdAccessor |
keyof TData & string |
'id' |
Property key used as the unique row identifier. |
isRowFilterable |
(row: TData) => boolean |
— | Predicate to exclude rows from column filtering. |
onRowClick |
(row: TData) => void |
— | Callback when a body row is clicked. |
rowClassName |
(row: TData) => string | undefined |
— | Return additional CSS class names per row. |
pinnedTopRows |
TData[] |
— | Rows pinned to the top, unaffected by sort/filter. |
loadingOverlay |
ReactNode |
Built-in skeleton | Custom loading overlay. |
errorOverlay |
(error: Error, retry: () => void) => ReactNode |
Built-in error | Custom error overlay. Receives the error and a retry callback. |
emptyOverlay |
ReactNode |
Built-in empty | Custom empty state overlay. |
className |
string |
— | Additional CSS class on the wrapper element. |
All types are exported from the package entry point.
import type {
// Component props
DataTableProps,
// API mode
DataTableDataSource,
DataTableQuery,
DataTableResponse,
DataTableSortModel,
SetFilterValuesQuery,
SetFilterValuesSource,
// Pagination
PaginationConfig,
// Filters
DataTableFilterModel,
SetFilterModel,
NumberFilterModel,
NumberFilterOperator,
// Export
ExportOptions,
// Re-exported from AG Grid
ColDef,
GridApi,
ICellRendererParams,
ValueGetterParams,
} from '@sandylib27/react-ag-datatable';interface DataTableQuery {
page: number;
pageSize: number;
sort: DataTableSortModel | null;
filters: Record<string, DataTableFilterModel>;
}
interface DataTableSortModel {
field: string;
direction: 'asc' | 'desc';
}
interface DataTableResponse<TData> {
rows: TData[];
totalRows: number;
}
type DataTableDataSource<TData> = (query: DataTableQuery) => Promise<DataTableResponse<TData>>;
interface SetFilterValuesQuery {
field: string;
filters: Record<string, DataTableFilterModel>;
}
type SetFilterValuesSource =
| Record<string, string[]>
| ((query: SetFilterValuesQuery) => Promise<string[]> | string[]);
interface PaginationConfig {
pageSize?: number;
pageSizeOptions?: number[];
}
// Filter models
interface SetFilterModel {
type: 'set';
values: string[];
}
interface NumberFilterModel {
type: 'number';
operator: NumberFilterOperator;
value?: number;
valueTo?: number; // used with 'between' operator
}
type NumberFilterOperator =
| 'equals'
| 'notEqual'
| 'greaterThan'
| 'greaterThanOrEqual'
| 'lessThan'
| 'lessThanOrEqual'
| 'between'
| 'blank'
| 'notBlank';
type DataTableFilterModel = SetFilterModel | NumberFilterModel;| Layer | Technology |
|---|---|
| Grid engine | AG Grid Community v35+ |
| UI primitives | Radix UI (bundled, no install needed) |
| Icons | Lucide React |
| Styling | Tailwind CSS + CSS variables |
| Bundler | tsup (ESM + CJS + DTS) |
| Dev server | Vite (examples only, not published) |
Contributions are welcome! Please open an issue or submit a pull request.
git clone https://github.com/sandylib27/react-ag-datatable.git
cd react-ag-datatable
npm install
npm run dev:examples # run examples in browser
npm run build # build the library
npm run type-check # verify types