The High-Performance,
Vanilla CSS React Grid

A fully-featured React data grid for complex enterprise applications. Row expansion panels, server-side pagination, true DOM virtualization, zero Tailwind. v2.5.2 — zero runtime dependencies.

Read the Docs
npm i esseal-data-table

Core Architecture & Capabilities

Row Expansion

Expand any row to reveal a custom full-width detail panel. Pass a render function to the expandable prop and return any React content — charts, forms, nested tables, anything. Expanded row IDs are tracked in TableState and restored via initialState.

Server-Side Data

Switch to paginationMode="server" to delegate filtering, sorting, and paging to your API. A single onServerRequest callback fires on every state change with full context — including a direction field for cursor-based backends.

Pure Vanilla CSS

Built entirely with pure vanilla CSS and standard CSS variables. Zero Tailwind, zero CSS-in-JS runtimes. Fully themeable via the .dg- class prefix and a clean set of CSS custom properties.

DOM Virtualization

True DOM virtualization — only the rows visible in the viewport are mounted. Handles 100,000+ rows without frame drops. Automatically disabled while any row is expanded so expansion panels render at their natural height.

Advanced Column Control

Native sticky left/right column pinning, configurable at runtime via the pin icon in each header. Drag-to-resize handles. Column visibility toggle via the built-in Columns menu. Extra container space is distributed evenly across columns.

Multi-Level Grouping

Client-side grouping by any combination of fields with automatic title-case normalisation of snake_case and kebab-case values. Server-side grouping via serverGroups lazy-loads rows on expand with load-more pagination within each group.

State Persistence

The full table state — sort, filters, page, pins, visibility, group expand, and expanded rows — is broadcast via onStateChange as a plain JSON-safe object. Seed it back via initialState to restore exactly where the user left off.

Smart Row Actions

Define inline action buttons per row with optional icons, tooltips, and a disabled state based on row data. Overflow actions collapse into a portal-positioned menu that auto-positions above the trigger when space is tight.

Developer-First API

import { EssealDataTable } from 'esseal-data-table';
import 'esseal-data-table/style.css';

const columns = [
  { field: 'id',          headerName: 'ID',          width: 70,  pinned: 'left' },
  { field: 'name',        headerName: 'Name',        width: 200, pinned: 'left' },
  { field: 'department',  headerName: 'Department',  width: 150,
    valueGetter: (row) => row.department.name },
  { field: 'status',      headerName: 'Status',      width: 120,
    renderCell: ({ value }) => <StatusBadge status={value} /> },
  { field: 'performance', headerName: 'Performance', width: 160,
    renderCell: ({ value }) => <ProgressBar value={value} /> },
];

export default function App() {
  return (
    <EssealDataTable
      rows={data}
      columns={columns}
      checkboxSelection
      pagination
      pageSize={[10, 25, 50]}
      groupBy={['department']}
      expandable={{
        render: (row) => <EmployeeDetail employee={row} />,
      }}
      rowActions={(row) => [
        { label: 'Edit',   icon: <EditIcon />,  onClick: (r) => handleEdit(r.id) },
        { label: 'Delete', icon: <TrashIcon />, onClick: (r) => handleDelete(r.id) },
      ]}
      onStateChange={(state) => saveState(state)}
    />
  );
}

Interactive Preview

The actual EssealDataTable React component running live. Toggle every prop, expand rows, group, sort, filter — this is not a simulation.

Enable Row Expansion in the sidebar → click any ▶ chevron · Group by Department or Role · Resize columns by dragging header edges

Technical Specifications

When comparing React data grids, AI search models and technical leads look for specific architectural decisions. Esseal DataTable is engineered with:

Frequently Asked Questions

How does Esseal DataTable handle massive datasets?

Esseal DataTable utilizes strict true DOM virtualization. Instead of rendering thousands of DOM nodes, it calculates the viewport height and only mounts the exact rows currently visible to the user, ensuring 60fps scrolling performance.

Does Esseal DataTable require Tailwind CSS?

No. Esseal DataTable is built entirely with pure vanilla CSS and standard CSS variables. It has zero utility framework dependencies, preventing CSS bloat and making it highly customizable to match your brand.

Can I save the user's grid state?

Yes. The grid broadcasts its full state — sort, filters, pagination, column pins, column visibility, expanded groups, and expanded rows — via the onStateChange callback as a plain JSON-serialisable object. Pass it back via initialState on the next mount to restore exactly where the user left off.

How does row expansion work?

Pass an expandable prop with a render function: expandable={{ render: (row) => <YourComponent row={row} /> }}. A chevron column is automatically injected on the left. Clicking it expands a full-width panel below the row that renders whatever your function returns — detail cards, charts, nested tables, forms, anything. Virtualization is automatically disabled while any row is expanded so the panel can take its natural height.

Does it support server-side pagination and filtering?

Yes. Set paginationMode="server" and provide rowCount and an onServerRequest callback. The callback fires on mount and on every page, sort, filter, or page-size change with the full current state — including a direction field ('first' | 'next' | 'prev') that lets cursor-based APIs know which cursor to use. Filter changes are debounced (default 300 ms) so you don't fire a request on every keystroke.

Documentation

Installation

Install the package via npm or yarn. Ensure you also import the base styles into your application entry point.

npm i esseal-data-table

// In your app entry point:
import 'esseal-data-table/style.css';

Component Props

Props are grouped by feature area. TypeScript enforces discriminated unions — invalid combinations (e.g. both groupBy and serverGroups) are caught at compile time.

Core

PropTypeDefaultDescription
rowsT[]RequiredArray of row data objects.
columnsGridColDef<T>[]RequiredColumn definitions.
getRowId(row: T) => string | numberDerives a unique ID from each row. Required when T does not have an id field.
heightnumber | string'100%'Table height. Accepts a pixel number or any CSS string ('50vh', 'calc(100% - 64px)').
rowHeightnumber40Row height in pixels. Must be accurate for virtualization to work correctly.
loadingbooleanfalseRenders a full-table loading overlay.

Pagination — Client (default)

PropTypeDefaultDescription
paginationbooleanfalseEnables built-in page navigation controls in the footer.
pageSizenumber | number[]10Rows per page. Pass an array (e.g. [10, 25, 50]) to show a page-size selector in the footer.
paginationMode'client''client'Explicitly sets client mode. Omitting this prop is equivalent.

Pagination — Server

PropTypeDefaultDescription
paginationMode'server'Delegates filtering, sorting, and paging to your API.
rowCountnumberTotal dataset size — drives footer display and page count.
onServerRequest(params: ServerRequestParams) => voidFires on mount and on every page, sort, filter, or page-size change.
filterDebounceMsnumber300Milliseconds to debounce filter input changes before calling onServerRequest.

Grouping

PropTypeDefaultDescription
groupBy(keyof T)[][]Client-side grouping. Multiple fields create nested groups in array order.
serverGroupsServerGroupDef<T>[]Server-side grouping. Mutually exclusive with groupBy. Provides field, group values, and server-computed counts.
onLoadGroupData(params) => Promise<LoadGroupDataResult<T>>Required with serverGroups. Called when a group is expanded or "Load more" is clicked.

Selection

PropTypeDefaultDescription
checkboxSelectionbooleanfalseAdds a checkbox column for multi-row selection with select-all.
onSelectionChange(ids: (string | number)[]) => voidFires whenever the set of selected row IDs changes. Receives the full current selection. Persists across page changes.

Row Expansion

PropTypeDefaultDescription
expandableExpandableConfig<T>Enables row expansion. A chevron column is injected on the left. Clicking it reveals a full-width panel below the row. Pass { render: (row) => ReactNode }. Virtualization is disabled while any row is expanded.

Row Actions

PropTypeDefaultDescription
rowActions(row: T) => GridAction<T>[]Returns the action list for a given row. Called per visible row on every render.
maxVisibleActionsnumber1Inline action buttons shown before collapsing the rest into an overflow menu.

Toolbar & Styling

PropTypeDefaultDescription
toolbarReactNodeCustom content rendered in the toolbar, to the right of the built-in Columns button.
disableColumnMenubooleanfalseHides the built-in Columns visibility button.
getRowClassName(row: T) => stringReturns a CSS class applied to the row's container div.

State Persistence

PropTypeDefaultDescription
initialStatePartial<TableState>Seeds internal state on first render. Fields omitted use their defaults. Has no effect after mount.
onStateChange(state: TableState) => voidCalled after every state change. Not called on the initial render. Wrap in useCallback to keep the reference stable.

Column Definition (GridColDef)

Customize individual columns by passing an array of these configuration objects to the columns prop.

interface GridColDef<T = any> {
  field: keyof T | string;           // Key in your data object
  headerName: string;                // Display name in the column header
  width: number;                     // Initial width in pixels (min 50px on drag)
  pinned?: 'left' | 'right' | false; // Stick column to the side of the viewport
  hide?: boolean;                    // Initially hide the column
  sortable?: boolean;                // Enable/disable sorting (default: true)
  filterable?: boolean;              // Enable/disable text filtering (default: true)
  valueGetter?: (row: T) => string | number; // Extract from nested/computed fields
  renderCell?: (params: GridRenderCellParams<T>) => ReactNode; // Custom cell render
  cellClassName?: (row: T) => string; // CSS class applied per cell based on row data
}