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:
GridGridOptionsColumnDefgrid.sort,grid.filter,grid.pagination,grid.columns,grid.scroll,grid.state,grid.exportgrid.on()andgrid.off()
Advanced integration hooks are also public:
grid.usePlugin()grid.getPluginHost()grid.getGridApi()
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
rowCountandcolCount- 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
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
<div id="grid"></div>#grid { width: 100%; height: 640px;}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 addressablecolCount: total columns the grid should renderrowHeight: row height as a single number ornumber[]colWidth: column width as a single number ornumber[]columns: optional metadata for headers, renderers, editors, sorting, filtering, resize, and drag behaviordataModeplusonDataRequest: the backend loading pathpagination,loading,columnResize,columnDrag, andinfiniteScrolling: higher-level behavior on top of the core grid
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().
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.
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.
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
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.
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
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);});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 aGridPlugingrid.getPluginHost()gives low-level plugin host accessgrid.getGridApi()exposes the internal API registry used by plugins
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
colCountsynced with the actual column schema - pass
any[][]tosetData()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.exportover older flat aliases - disable defaults explicitly when you need to, for example
enableSelection: falseorenableColumnDrag: false