Documentation

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 NoderenderCell 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.

  • Row context menurowMenu (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: false turns it off.
  • Toolbartoolbar: true adds 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

ReactJavaScript
Engine, stylesheet, themes, workersharedshared
Config / props<DataGrid {...props} />createGrid(host, props)
Custom cell contentReact nodestring or DOM Node
Imperative APIonReady(controller)grid.controller
Server-side renderingrenders markupconstructs 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.