JavaScript
The DataGrid does not need React. @kanunilabs/datagrid is a second renderer
over the same engine (@kanunilabs/datagrid-core) and the same
stylesheet — virtualization, sorting, filtering, grouping, pinning, the
worker offload and theming behave identically, and a conformance suite in CI
keeps the two renderers from drifting.
npm i @kanunilabs/datagrid
Try it live — 100,000 rows driven by
createGrid(), no React on the page.
A grid in one call
import { createGrid } from '@kanunilabs/datagrid';
import '@kanunilabs/datagrid/styles.css';
const grid = createGrid(document.getElementById('host'), {
gridId: 'orders',
dataSource: rows, // your array
rowKey: 'id',
columns: [
{ field: 'id', headerName: 'ID', dataType: 'number', width: 80 },
{ field: 'product', headerName: 'Product', width: 220 },
{ field: 'units', headerName: 'Units', dataType: 'number', width: 100 },
{ field: 'unitPrice', headerName: 'Unit price', dataType: 'number',
valueFormatter: (v) => `$${Number(v).toFixed(2)}` },
],
theme: 'quartz',
toolbar: true,
filterRow: true,
headerFilter: true,
selection: { mode: 'multiple' },
});
grid.controller.setSort([{ columnId: 'units', direction: 'desc' }]);
grid.setData(nextRows); // live data
grid.destroy(); // when the host goes away
The config mirrors the React props, DOM-flavoured. Where React takes a
React node, vanilla takes a string or a DOM Node — renderCell returns
either, and the string form is set as textContent, so there is no HTML
string API and no injection surface.
The overlap is not total, and the renderer tells you where it ends rather than
letting you guess: pass a React-only option and it says so in the console,
naming what to write instead. Two column switches are spelled differently —
React's columnResize / columnReorder are resizable / reorderable here.
Every option in the React prop table now has an answer here — the last one,
renderRow, landed in #142. What remains in the console warning are spellings,
not gaps.
renderRow draws the same line React does: the template replaces the row's
CELLS — including the row number, the detail expander and the selection
checkbox — while the row element itself stays, carrying the height, the aria
indices and the classes the virtualizer and selection depend on. So in-cell
editing and range selection have nothing to attach to while it is on. Return
the SAME node you returned last time and the DOM is left alone, which is what
lets a focused input inside a template keep its caret across a sort.
Two differences are worth knowing rather than discovering. focusedRowKey is
the STARTING focus here, applied once — React re-applies it whenever the prop
changes, and a vanilla config is read once; later moves go through
grid.controller.focusRow(key). And showRowCount, retired in both renderers
on 2026-08-15, prints a one-line notice here when you set it — a React user
reads the @deprecated tag in their editor, and someone writing plain JS
against the UMD bundle has no such surface. The row count lives in the status
bar (Enterprise) or the pager.
Everything the engine does — virtualization, sorting, filtering, grouping, pinning, the worker, export — is identical in both, because both call the same controller.
grid.controller is the engine itself — the same object the React binding
hands to onReady. Everything imperative (search, filters, selection, export,
scrolling, state) lives there; see the
API reference.
Menus and extension points
- Row context menu —
rowMenu(an array, or a function of the clicked row) appends your items above the grid's own Copy/Export block. - Header context menu — right-click a header for sort, group, pin, hide,
best-fit and move;
headerMenu(context, defaults)reshapes the list,headerMenu: falseturns it off. - Toolbar —
toolbar: trueadds quick search, the column chooser and an export menu;toolbarItemsAfter: [button, …]appends your own controls.
Script tag (UMD)
No bundler? The package ships a self-contained build:
<link rel="stylesheet" href="https://unpkg.com/@kanunilabs/datagrid/dist/styles.css" />
<script src="https://unpkg.com/@kanunilabs/datagrid/dist/datagrid.umd.js"></script>
<script>
const { createGrid } = KanuniLabsDataGrid;
createGrid(document.getElementById('host'), { /* the same config */ });
</script>
The engine is bundled in; the optional export peers (exceljs, jsPDF) are not, and degrade with an actionable error if an export needs them.
Enterprise edition
@kanunilabs/datagrid-enterprise wraps the Community grid — it does not fork
it — and adds the paid surface: cell / batch / row / form / popup editing
with validation and undo, range selection, fill handle and clipboard,
master-detail, tree data, cell spans and notes, charts and sparklines, the
filter builder, the import wizard, styled Excel/PDF export, status and side
bars. It is installed from the
private registry with your license.
import { createEnterpriseGrid } from '@kanunilabs/datagrid-enterprise';
import '@kanunilabs/datagrid/styles.css';
import '@kanunilabs/datagrid-enterprise/styles.css';
const grid = createEnterpriseGrid(host, {
licenseKey: 'KLAB1.…',
// everything createGrid takes, plus:
editing: {
mode: 'batch', // 'cell' | 'batch' | 'row' | 'form' | 'popup'
commandColumn: true,
allowAdding: true,
allowDeleting: true,
rules: { product: [{ type: 'required' }] },
onSave: (changes) => api.save(changes),
},
statusBar: true,
sideBar: true,
charting: { crossFilter: true },
filterBuilder: true,
onImport: (rows) => append(rows),
toolbarExcel: true,
toolbarPdf: true,
// styledExport defaults to true — Excel carries the grid's formatting.
});
grid.chartPanel?.open({ categoryColumnId: 'category' });
grid.openFilterBuilder();
grid.isEnterpriseLicensed();
Without a valid key the grid still works, watermarked — your app never breaks on a licensing problem. One license key opens both Enterprise packages, React and plain JavaScript alike; see Enterprise & licensing.
The Enterprise UMD build is dist/datagrid-enterprise.umd.js
(global KanuniLabsDataGridEnterprise); it bundles the Community renderer and
the licensing core, so one script tag is enough.
What is the same, what is different
| React | JavaScript | |
|---|---|---|
| Engine, stylesheet, themes, worker | shared | shared |
| Config / props | <DataGrid {...props} /> | createGrid(host, props) |
| Custom cell content | React node | string or DOM Node |
| Imperative API | onReady(controller) | grid.controller |
| Server-side rendering | renders markup | constructs on mount (needs a document) |
| Enterprise | <EnterpriseDataGrid> | createEnterpriseGrid() |
SSR is the one honest difference: the JavaScript renderer produces DOM, so it is created when the host element exists. Everything else is held to parity by the conformance suites that run on every commit.