Core Grid System

Learn the public entry points for ZenGrid core, including Grid, GridOptions, ColumnDef, lifecycle, data flow, and namespaced APIs.

The Core Grid System is the developer-facing foundation of ZenGrid. In normal app code you work with the Grid class, GridOptions, ColumnDef, grid events, and the namespaced APIs exposed on each grid instance.

Public Entry Points

For most applications, the core public surface is:

  • Grid
  • GridOptions
  • ColumnDef
  • grid.sort, grid.filter, grid.pagination, grid.columns, grid.scroll, grid.state, grid.export
  • grid.on() and grid.off()

Advanced integration hooks are also public:

  • grid.usePlugin()
  • grid.getPluginHost()
  • grid.getGridApi()
Info

Internal setup helpers exist in the codebase, but the supported app-level entry point is still new Grid(container, options).

Core Mental Model

ZenGrid is container-first and viewport-driven. You supply:

  • a real DOM container with explicit dimensions
  • rowCount and colCount
  • row and column sizing
  • optional column metadata
  • either in-memory row data or a backend loader

Then you call render() and work through the grid instance.

Create a Grid

basic-grid.ts
import { Grid, type ColumnDef } from '@zengrid/core';
import '@zengrid/core/dist/styles.css';
const container = document.getElementById('grid')!;
const columns: ColumnDef[] = [
{ field: 'id', header: 'ID', width: 80, renderer: 'number', sortable: true },
{ field: 'name', header: 'Name', width: 220, renderer: 'text', sortable: true, editable: true },
{ field: 'status', header: 'Status', width: 140, renderer: 'chip', filterable: true },
];
const rows = [
[1, 'Ada Lovelace', 'Active'],
[2, 'Grace Hopper', 'Active'],
[3, 'Katherine Johnson', 'Paused'],
];
const grid = new Grid(container, {
rowCount: rows.length,
colCount: columns.length,
rowHeight: 36,
colWidth: columns.map((column) => column.width ?? 140),
columns,
enableKeyboardNavigation: true,
enableSelection: true,
overscanRows: 10,
overscanCols: 5,
});
grid.setData(rows);
grid.render();

Container Requirements

index.html
<div id="grid"></div>
styles.css
#grid {
width: 100%;
height: 640px;
}
Warning

ZenGrid will not size the container for you. If the container has no usable height or width, viewport calculation and rendering break down immediately.

What The Options Really Mean

The core options you should think about first are:

  • rowCount: total rows the grid should treat as addressable
  • colCount: total columns the grid should render
  • rowHeight: row height as a single number or number[]
  • colWidth: column width as a single number or number[]
  • columns: optional metadata for headers, renderers, editors, sorting, filtering, resize, and drag behavior
  • dataMode plus onDataRequest: the backend loading path
  • pagination, loading, columnResize, columnDrag, and infiniteScrolling: higher-level behavior on top of the core grid
💡 Tip

Even when you provide columns, keep colCount aligned with columns.length. The core grid still relies on colCount for rendering and feature setup.

Data Model: 2D Arrays

The core grid stores cell data as any[][]. The simplest frontend flow is to transform your source records into rows and pass them to setData().

map-records-to-rows.ts
const employees = [
{ id: 1, name: 'Ada Lovelace', status: 'Active' },
{ id: 2, name: 'Grace Hopper', status: 'Active' },
];
const rows = employees.map((employee) => [
employee.id,
employee.name,
employee.status,
]);
grid.setData(rows);
grid.render();

ColumnDef.field is still important for column identity, header behavior, filtering state, and persistence, but setData() itself expects a matrix of cell values, not an array of objects.

Frontend Mode vs Backend Mode

Use frontend mode when all rows can live in memory. Use backend mode when the grid should request windows of rows on demand.

backend-grid.ts
import { Grid, type ColumnDef, type DataLoadRequest, type DataLoadResponse } from '@zengrid/core';
const columns: ColumnDef[] = [
{ field: 'id', header: 'ID', width: 80 },
{ field: 'name', header: 'Name', width: 220 },
{ field: 'status', header: 'Status', width: 140 },
];
const grid = new Grid(container, {
rowCount: 100000,
colCount: columns.length,
rowHeight: 36,
colWidth: columns.map((column) => column.width ?? 140),
columns,
dataMode: 'backend',
onDataRequest: async ({ startRow, endRow, query, signal }: DataLoadRequest): Promise<DataLoadResponse> => {
const response = await fetch('/api/users/window', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal,
body: JSON.stringify({ startRow, endRow, query }),
});
const payload = await response.json();
return {
data: payload.rows,
totalRows: payload.totalRows,
startRow,
endRow,
};
},
});
grid.render();

For backend mode, these methods matter:

  • grid.getDataMode()
  • grid.getDataStatus()
  • grid.refreshData()
  • grid.retryDataRequest()

dataMode: 'auto' resolves to backend when onDataRequest is present and frontend otherwise.

Column Definitions

ColumnDef is the bridge between raw cell values and user-visible behavior.

columns.ts
import type { ColumnDef } from '@zengrid/core';
const columns: ColumnDef[] = [
{
field: 'id',
header: 'ID',
width: 80,
renderer: 'number',
sortable: true,
resizable: true,
},
{
field: 'name',
header: 'Name',
width: 220,
renderer: 'text',
editable: true,
filterable: true,
},
{
field: 'notes',
header: 'Notes',
width: 320,
overflow: { mode: 'wrap', maxLines: 3 },
autoHeight: true,
},
];

header can be a simple string or a richer header configuration object when you need custom header rendering or behavior.

Lifecycle and Common Operations

lifecycle.ts
grid.render();
grid.refresh();
grid.updateOptions({ overscanRows: 14, overscanCols: 6 });
grid.updateCells([{ row: 12, col: 2 }]);
grid.clearCache();
const dimensions = grid.getDimensions();
const stats = grid.getStats();
grid.destroy();

Use render() for the initial mount. Use refresh() when data or state changed and you need a redraw. Always call destroy() when the grid leaves the page or component tree.

Preferred Instance APIs

The grid instance exposes namespaced APIs for the most common operations.

instance-apis.ts
grid.sort.toggle(0);
grid.filter.set(2, 'contains', 'Active');
grid.pagination.goTo(2);
grid.columns.autoFitAll();
grid.scroll.toCell(500, 1);
const snapshot = grid.state.getSnapshot();
const csv = grid.export.csv({ rows: 'filtered', columns: 'visible' });

Backward-compatible flat aliases such as toggleSort(), getStateSnapshot(), and exportCSV() still exist, but the namespaced APIs above are the preferred surface.

Events

events.ts
grid.on('cell:click', ({ cell, value }) => {
console.log('clicked', cell, value);
});
grid.on('cell:doubleClick', ({ cell }) => {
console.log('double click', cell);
});
grid.on('selection:change', ({ ranges }) => {
console.log('selection', ranges);
});
grid.on('column:resize', ({ column, newWidth }) => {
console.log(`column ${column} is now ${newWidth}px`);
});
grid.on('header:sort:click', ({ columnIndex, nextDirection }) => {
console.log('sort request', columnIndex, nextDirection);
});
Info

The public double-click event is cell:doubleClick. Older cell:dblclick examples do not match the current event map.

Advanced Extension Points

If you are building plugins or deep integrations:

  • grid.usePlugin(plugin) installs a GridPlugin
  • grid.getPluginHost() gives low-level plugin host access
  • grid.getGridApi() exposes the internal API registry used by plugins
Info

Most applications do not need PluginHost or GridApiImpl directly. Treat them as extension hooks, not the everyday runtime API.

What Developers Need To Get Right

  • give the container explicit dimensions before calling render()
  • keep colCount synced with the actual column schema
  • pass any[][] to setData() in frontend mode
  • call render() once the grid is configured
  • call destroy() when the grid is removed
  • prefer grid.sort / grid.filter / grid.state / grid.export over older flat aliases
  • disable defaults explicitly when you need to, for example enableSelection: false or enableColumnDrag: false