1.113.1 Data Grids#
Framework-free JavaScript data grids compared: Tabulator, AG Grid Community, DataTables 3, Handsontable, SlickGrid, RevoGrid, Grid.js. Four rendering strategies measured at 1k–100k rows.
At a glance#
Findings checked against this survey’s current text on 2026-09-02.
| Library | How it works | Best for |
|---|---|---|
| Tabulator | Virtual-window renderer, 37 modules, one MIT file; UMD global Tabulator | Client-side reports: grouping, column totals and CSV/XLSX download are all in the core |
| AG Grid Community | Virtual rows and columns; client-side and infinite row models; Theming API injects CSS | Keyboard and screen-reader work on a free grid; the strongest maintenance signal here |
| DataTables | Enhances a native <table>; paged by default; the serverSide request contract | Server-rendered list pages over a database; the cheapest exit in the category |
| Handsontable | Spreadsheet: range selection, clipboard, fill, undo, validators; virtual rows; 3 themes × light/dark | Data entry that feels like Excel, for non-commercial use or under a per-developer license |
| SlickGrid | DataView + virtual viewport, the original architecture; TypeScript, IIFE and ESM; SortableJS as a global | Teams that want the virtual renderer and the DataView and will write the rest |
| RevoGrid | <revo-grid> custom element built with Stencil; ESM entry plus 22 lazy chunks; virtual rows and columns | Pages built from custom elements and custom properties |
| Grid.js | Bundled Preact renders every row, or one page with pagination; server is a fetch wrapper | A short list on a content page, with the stall priced in |
| Toast UI Grid | Archived; requires three unbundled externals including xlsx ^0.17.1 | Migrating away from |
| Cheetah Grid | Canvas: no row DOM at all | Raw scroll speed where the accessibility tree does not matter |
| regular-table | FINOS custom element rendering a virtual native <table> from a data-listener callback | Building your own grid on a 10 KB virtual table |
| simple-datatables | DataTables-shaped vanilla grid; paged by default | A DataTables-style table where LGPL is acceptable |
| TanStack Table core | Headless: computes row models, renders nothing; a vanilla example ships in the repo | Owning the <table> markup and borrowing the sort/filter/group logic |
What the research found
- The category has four rendering strategies — virtual window, paged, full render, canvas — and the strategy predicts a grid’s 100,000-row behavior better than its feature list does. — A paged grid and a virtual grid both claim a million rows; only one can scroll to row 500,000. Pick the strategy before the library.
- DataTables 3.0.0 (2026-07-24) has no jQuery dependency; the UMD build assigns window.DataTable with no jQuery argument. — Every ‘jQuery DataTables’ tutorial describes 2.x. The oldest grid here is now a 44 KB framework-free one with the most specified server contract.
- Grid.js has merged 0 pull requests and landed 0 commits since 2026-03-01 against 32 open PRs; its last release is from March 2024. — The stalled cell of the four-cell test. 177,477 monthly installs measure exposure, not choice. Not for a new page without knowing this.
- The free tiers are three different things: MIT throughout; MIT core with grouping, pivot, Excel and server-side data paid (AG Grid Enterprise, RevoGrid Pro); non-commercial only (Handsontable). — A reader who picks the most popular grid and adds a group-by button meets a watermark. Read the line before writing column definitions.
- AG Grid Community’s script-tag build is 495 KB gzipped, not the 362 KB the triggering pass reported; the smaller figure is the CommonJS bundler entry. — Vendor size claims name a file without saying which. The file a page loads is fifty times Grid.js and five times DataTables for the same list.
What the research recommends
Default — DataTables for a server-backed list; Tabulator for a client-side report with grouping and export; AG Grid Community when keyboard and screen-reader work matters more than bytes. Decide the rendering strategy first.
| Use case | Pick |
|---|---|
| server rendered list | DataTables — the serverSide contract, 44 KB, enhances a native table; Tabulator’s remote modes second |
| client side report | Tabulator — GroupRows, ColumnCalcs and Download in one MIT file; SlickGrid’s DataView second |
| accessibility audit | AG Grid Community — WCAG 2.0 AA claim, roles, keyboard focus moves; a native-table grid (DataTables) for small data |
| spreadsheet | Handsontable if the license permits; RevoGrid’s MIT core for range editing without formulas |
| large client side | A virtual grid from the L2 table — SlickGrid, Tabulator, AG Grid, RevoGrid, Handsontable — never full-render or paged |
| runtime theme | RevoGrid or AG Grid — custom properties that cascade from the container; Handsontable’s must be set on its own root |
| small content list | DataTables at 44 KB and maintained; Grid.js at 19 KB with the stall priced in |
Avoid
| Library | Reason | Risk |
|---|---|---|
| Toast UI Grid | Repository archived 2024; xlsx pinned to a vulnerable range it cannot leave | HIGH |
| Grid.js for a new product | No merges or commits since March 2026; last release March 2024 | MEDIUM |
| Any full-render configuration past 10,000 rows | Grid.js and DataTables with paging off did not finish 100,000 rows in 60 s in S2 | MEDIUM |
first paint 100k rows ms
- regular-table: 22
- cheetah-grid canvas: 39
- slickgrid: 46
- tabulator: 195
- ag-grid-community: 212
- revogrid: 224
- datatables paged: 1061
- handsontable: 1119
- simple-datatables paged: 17602
- TIMED OUT: grid.js (every row rendered), datatables paging:false, simple-datatables paging:false – none finished 100,000 rows in 60 s
first paint 10k rows ms
- slickgrid: 38
- regular-table: 26
- cheetah-grid canvas: 43
- tabulator: 116
- ag-grid-community: 117
- datatables paged: 167
- revogrid: 213
- handsontable: 248
- simple-datatables paged: 1316
- datatables paging-false: 2740
- simple-datatables paging-false: 1818
- gridjs every-row: 4543
sort 100k rows ms
- datatables paged: 199
- slickgrid: 232
- tabulator: 527
- ag-grid-community: 689
- handsontable: 1200
- simple-datatables paged: 1240
dom nodes in grid at 100k rows
- cheetah-grid canvas: 5
- datatables paged: 238
- slickgrid: 305
- regular-table: 317
- handsontable: 338
- revogrid: 345
- ag-grid-community: 404
- tabulator: 1082
- full-render at 10k: 90128
script tag delivery gz kb
- gridjs: 19
- simple-datatables: 28
- datatables with dt-stylesheet: 44
- cheetah-grid: 86
- tabulator: 102
- slickgrid 7 files: 114
- revogrid all 23 chunks: 200
- handsontable full: 462
- ag-grid-community: 495
axe core violations at 1k rows
- ag-grid-community: 0
- regular-table: 0
- gridjs: 0
- tabulator: 1 rule (1 node): aria-required-children critical
- slickgrid: 1 rule (1 node): aria-required-children critical
- handsontable: 2 rules (2 nodes): aria-required-children critical, scrollable-region-focusable serious
- datatables: 3 rules (11 nodes): aria-prohibited-attr serious x8, aria-required-children critical, presentation-role-conflict minor x2
- revogrid: 5 rules (52 nodes): aria-valid-attr-value critical x34, aria-required-parent critical x8, button-name critical x8, aria-required-children, scrollable-region-focusable
keyboard arrow moves focus between cells
- ag-grid-community: yes
- handsontable: yes
- tabulator: no – focus stays on the table holder
- slickgrid: no – focus in grid, cell does not change
- revogrid: no – focus in grid, cell does not change
- datatables: no – no cell focus without the KeyTable extension
- gridjs: no
Explainer
Data grids without a framework#
What a data grid is#
A data grid is the component that turns an array of records into a table a person can
work with: column headers that sort when clicked, rows that scroll, cells that can be
filtered, selected, edited, grouped and exported. A plain HTML <table> shows data. A
grid lets someone do something with ten thousand rows of it.
Every grid has the same parts under different names:
- Rows and columns. The data is an array of objects; each column definition names a field and says how to show it.
- A viewport. The visible region. What a grid does with the rows that are not in the viewport is the single most important design decision in the category, and it is covered below.
- A data model. Where sorting, filtering, grouping and paging happen — in the browser over the whole array, or on a server that sends one page at a time.
- An accessibility tree. The
role="grid",row,columnheaderandgridcellattributes and the keyboard behavior that let a screen reader and a keyboard user navigate it. A grid built from<div>elements has none of this unless the library writes it.
Framework-free means the library draws its own DOM#
Most grids on npm are components: a React grid is a React component and needs React
mounted to exist. A framework-free grid is a library you load with a <script> tag or a
single import, point at an element, hand an array, and it draws. It may ship React or
Vue wrappers as well, but those are thin adapters over a core that does not need them.
The distinction matters to a specific kind of page: one rendered on the server (Django, Rails, Flask, Go templates, PHP) and enhanced in the browser with something small — htmx, Alpine.js, Hotwire, or nothing. Those pages have no bundler, no JSX, and no component tree to mount into. They have an element and an array, which is what a framework-free grid asks for.
The four rendering strategies#
Every library in this category says it handles large data. There are four ways to mean that, and a reader can predict most of a grid’s behavior from which one it uses.
Virtual window. The grid creates DOM only for the rows in the viewport, plus a
buffer above and below, and recycles those elements as the user scrolls. A hundred
thousand rows costs a hundred thousand objects in memory and about thirty <div>s on
the page. The scrollbar represents the whole dataset, and dragging it to
the middle shows row fifty thousand. The costs are that the browser’s find-in-page only
sees the rendered rows, that screen readers see only the rendered rows, and that the
library has to do work on every scroll event. Tabulator, AG Grid, Handsontable,
SlickGrid, RevoGrid and regular-table do this.
Paged. The grid renders one page — ten, twenty-five, fifty rows — and a pager. The rest of the data is an array, or is still on the server. Paging is cheap, accessible and old; every user has seen it. The cost is that a hundred thousand rows are two thousand pages, and “show me row fifty thousand” is a search, not a scroll. DataTables and simple-datatables page by default; Grid.js pages when told to.
Full render. Every row becomes a <tr>. This is what a plain <table> does and it
is fine up to some number of rows that depends on the browser, the columns and the
machine. Past that number the page stalls while the DOM is built, then scrolls slowly
because the browser is laying out every row. Grid.js does this by default; DataTables
does it when paging is turned off. The S2 pass measures where the number is.
Canvas. No row DOM at all. The grid paints cells onto a <canvas> element, so there
are no elements to lay out and scrolling is repainting. It is the fastest strategy and
the least accessible by default: a canvas has no text, no cells and no roles unless the
library maintains a parallel accessibility tree. Cheetah Grid is the canvas grid here.
Where the data model lives#
The second decision is where sorting, filtering and paging happen.
Client-side means the whole array is in the browser and the grid sorts it in memory. Simple, instant, and fine until the array is too big to download, at which point every page load pulls a database table across the network to show ten rows of it.
Server-side means the grid sends its state — which page, which sort column, which
filter — to a URL and receives one page of rows plus a count. The browser never holds
more than a page. Two libraries here specify this contract in detail (DataTables, with
a decade of server libraries built to it; Tabulator, with remote modes for paging, sort
and filter). Others fetch ranges (AG Grid’s infinite row model) or leave it to the
application.
The two decisions are independent. A paged grid can page client-side or server-side. A virtual grid can virtualize a client-side array or a server-side range cache. The combination a page needs follows from one question: should the browser ever hold every row?
Accessibility is a property of the rendered tree, not of the claim#
Two vendors in this category claim WCAG AA conformance. Several say nothing. What a
screen reader gets is determined by the DOM the grid writes: whether it is a native
<table> (which carries semantics for free), whether <div> rows carry
role="row" and role="gridcell", whether the current cell takes keyboard focus and
the arrow keys move it, and whether aria-rowcount tells the reader how many rows
exist beyond the ones rendered.
Virtual rendering and accessibility pull against each other. A screen reader walks the accessibility tree, and a virtual grid’s tree has thirty rows in it. One vendor in this survey says so in its own documentation and recommends disabling virtualization when accessibility matters. That is a trade the page has to make on purpose.
Theming by custom property#
Three of the libraries here define their colors, spacing and fonts as CSS custom
properties (--ag-header-background-color, --ht-header-background-color,
--revo-grid-header-bg), which means a page can restyle the grid from its own
stylesheet at runtime, including for a dark mode toggle. The others use SCSS variables
compiled into a stylesheet, or class overrides. The difference is whether a theme is
something the page controls or something the library ships.
Licenses in this category#
The category’s free tiers are not one thing. Some libraries are MIT throughout. Some are MIT at the core with the reporting features — grouping, pivot, Excel export, server-side data — sold in a separate package per developer. One is free only for non-commercial use and licensed for everything else. One is LGPL. A reader who needs group-by and a download button should find out which tier that lives in before choosing.
Terms used in this survey#
- Virtual DOM / virtual scrolling / row virtualization — the virtual window strategy above. Not related to React’s “virtual DOM”.
- Row model — AG Grid’s name for the data-model choice: client-side, infinite, server-side, viewport.
- Server-side processing — DataTables’ name for its server contract.
- Headless — a library that computes sorted and filtered row models but renders nothing; the page writes the markup. TanStack Table is the reference case.
- The stall test — this corpus’s durability check: pull requests still arriving but
none merged for months. Described in
ADDING-RESEARCH.md. - Custom element — a browser-native component (
<revo-grid>) registered withcustomElements.define, which is how RevoGrid is framework-free.
S1: Rapid Discovery
AG Grid Community#
The most-installed data grid on npm, MIT at the Community tier, with the features a reporting page wants — grouping, pivot, Excel, server-side row model — held in the Enterprise package. It is framework-free at the core: React, Angular and Vue wrappers sit on a vanilla API that a script tag reaches directly.
Registry#
| version | 36.1.0, released 2026-08-05 |
| license | MIT (Community); ag-grid-enterprise is a commercial EULA |
| dependencies | ag-stack, ag-charts-types |
| downloads, last month | 12,666,018 (Community); 5,968,698 (Enterprise) |
| stars | 15,578 |
| open issues / open PRs | 68 / 63 |
| PRs merged / commits since 2026-03-01 | 1,509 / 1,546 |
| releases in 12 months | 14 |
Source: observed-data.md.
Community and Enterprise#
The line matters more here than anywhere else in the survey, because most of what a reader means by “reporting” is on the far side of it.
| Community (MIT) | Enterprise (license key) |
|---|---|
| sorting, text/number/date column filters, quick filter | set filter (the default filter in Enterprise) |
| pagination, CSV export | Excel export |
| client-side and infinite row models | server-side and viewport row models |
| cell editing, cell renderers, column pinning and resizing | row grouping, aggregation, pivot |
| tree data | master/detail, range selection, clipboard |
| keyboard navigation, accessibility | column tool panel, context menus, integrated charts |
Source: ag-grid.com/javascript-data-grid/community-vs-enterprise/ and /filter-set/, accessed 2026-09-02. Enterprise is “licensed on a per-developer, per-deployment basis”, perpetual with a year of updates; an unlicensed Enterprise build “will display a watermark and an error message in the console” (source: ag-grid.com/javascript-data-grid/licensing/, accessed 2026-09-02). No price is on the page.
Loading it from a script tag#
dist/ag-grid-community.min.js defines window.agGrid, and agGrid.createGrid(element, options) is the whole API surface for mounting. Since v33 no stylesheet is needed: the
Theming API injects CSS from a theme object, Quartz by default, and the legacy ag-grid.css
plus ag-theme-*.css files still ship for the pre-v33 path (source:
ag-grid.com/javascript-data-grid/theming/, accessed 2026-09-02). The package declares
neither browser, unpkg nor jsdelivr.
Rendering#
Row and column virtualization in the client-side row model, on by default. The infinite row model fetches blocks as the user scrolls and is Community; the server-side row model, which adds server-side grouping and aggregation, is Enterprise (source: ag-grid.com/javascript-data-grid/row-models/, accessed 2026-09-02).
Remote data#
Two of the four row models talk to a server. Infinite (Community) asks for row ranges by
start and end index with the sort and filter model attached. Server-side (Enterprise)
adds group keys and aggregation requests. A Community page that wants server-side sort
and filter uses the infinite model or refetches rowData itself.
Accessibility#
The vendor’s claim is the strongest in the category: the grid meets “WCAG 2.0 level AA
guidelines” and thereby “the ADA and Section 508 standards”, uses role="grid" or
role="treegrid" with row and gridcell, and is tested with JAWS and VoiceOver. The
same page lists limitations: grouped column names can be misannounced, focus changes
are not always recognized, and row counts are “not supported” under the server-side row
model (source: ag-grid.com/javascript-data-grid/accessibility/, accessed 2026-09-02).
Seven accessibility issues are open and 139 have been filed in total — the largest
accessibility issue history here, which reads as the most users exercising it (source:
api.github.com search, accessed 2026-09-02).
Theming#
195 custom properties in styles/ag-grid.css (measured 2026-09-02; ag-grid-community
36.1.0). Under the Theming API the same --ag-* names are the parameters, set on the
theme object or on the container. Four legacy themes ship: Quartz, Alpine, Balham,
Material.
Advisories#
Two, both prototype pollution, both 2024: CVE-2024-38996 (high) in _.mergeDeep and
CVE-2024-39001 (medium), fixed in 31.3.4 and 32.0.1 (source: api.github.com/advisories,
accessed 2026-09-02). Nothing since.
Where it sits#
The most capable free grid here on rendering, keyboard and accessibility, at the price of a 495 KB gzipped script and a feature line that puts grouping, pivot and Excel on the paid side. A page that needs those has to decide whether they are worth a per-developer license or a different library.
Also in the category#
Libraries that pass or nearly pass the boundary test and were observed but not given a
full pass. Registry figures are from observed-data.md (accessed 2026-09-02) unless
marked.
Open source, alive#
Cheetah Grid (cheetah-grid 2.2.0, MIT, Future Architect). Renders to a single
<canvas>; there is no row DOM at all, which is the fourth rendering strategy in this
survey and the reason it is in S2’s harness. 5,266 installs a month, 1,536 stars, one
release in twelve months, no dependencies. 86 KB gzipped with its stylesheet (measured
2026-09-02; cheetah-grid 2.2.0; gzip -9). Six custom properties, all layout. A canvas
grid has no accessibility tree unless the library builds one; S2 checks.
regular-table (regular-table 0.9.0, Apache-2.0, FINOS). A custom element that
renders “a regular HTML <table> to a sticky position within a scrollable viewport”
where “only visible cells are rendered and queried from a natively async virtual data
model” (source: github.com/finos/regular-table README, accessed 2026-09-02). It is the
smallest thing here at 10 KB gzipped (measured 2026-09-02; regular-table 0.9.0). It has
no sort, filter or column API: the page supplies a data listener and does the rest. It is
the table under Perspective’s viewer.
simple-datatables (simple-datatables 10.3.0, LGPL-3.0, Fidus Writer). “Similar
to jQuery DataTables for use in modern browsers, but without the jQuery dependency”
(source: github.com/fiduswriter/simple-datatables README, accessed 2026-09-02) — a
description written before DataTables 3 made the distinction moot. 80,225 installs a
month, dependencies on dayjs and diff-dom. The license is the reader’s decision: LGPL
in a browser bundle is an obligation the other eleven libraries do not impose.
TanStack Table core (@tanstack/table-core 9.2.4, MIT). 81,836,911 installs a month,
28,398 stars, 255 PRs merged since March — by every registry measure the largest project
on this page, and it fails the boundary test on purpose. It is headless: it computes
sorted, filtered, grouped, paginated row models and draws nothing. The repository ships
a vanilla example with sorting, pagination, aggregation and basic rendering (source:
api.github.com contents of examples/vanilla, accessed 2026-09-02). A team that wants to
own its <table> markup and borrow the row-model logic starts here; a team that wants a
grid on screen does not. The core entry is 1 KB gzipped because it re-exports; the real
size is the bundle a page assembles.
Frappe DataTable (frappe-datatable 1.20.7, MIT). The grid inside ERPNext; 300,208
installs a month, 11 PRs merged since March. Three dependencies. Documented as a
component of its parent product more than as a standalone grid.
w2ui (w2ui 2.0.0, MIT). A jQuery-era UI kit whose 2.0 dropped jQuery. Last release
2023-04-26; 64 commits and 8 merges since March, so alive in the repository and silent on
the registry. 4,283 installs a month.
Perspective (@finos/perspective-viewer, FINOS, Apache-2.0). A WebAssembly pivot
engine with a viewer element; a different weight class and a different job (analytics,
not a list). The npm package’s latest is 3.8.0 from 2025-09-03 while the repository —
now perspective-dev/perspective — released v5.3.0 on 2026-08-25 (source: registry and
api.github.com, accessed 2026-09-02); the package has moved and the old name is stale.
Not resolved further here.
Open source, stalled or finished#
Read with ADDING-RESEARCH.md’s four-cell test: patches arriving and none merged is a
stall; nothing arriving is finished or forgotten.
| library | last release | open PRs | merged since 2026-03-01 | reading |
|---|---|---|---|---|
| Grid.js | 2024-03-03 | 32 | 0 | stalled (core set, see gridjs.md) |
| canvas-datagrid | 2023-05-22 | 6 | 0 | stalled; 142 open issues |
| List.js | 2021-01-17 | 23 | 0 | stalled; and a list filter, not a grid |
| jsGrid | 2016-12-15 | 21 | 0 | finished a decade ago; 81,235 installs a month regardless |
| Glide Data Grid | 2024-02-03 | 37 | 0 | stalled; React-only, out of scope on both counts |
| Toast UI Grid | 2024-01-10 | 28 | 0 | archived (core set, see toast-ui-grid.md) |
Commercial#
Not surveyed on features — that is the 3.xxx series’ line — but their license terms are facts a reader choosing a free grid needs, because three of them advertise a free tier.
Syncfusion Grid (@syncfusion/ej2-grids 34.2.6). Framework-free build exists. The
Community License is free for “Companies and individuals with less than $1 million USD
in annual gross revenue, 5 or fewer developers, and 10 or fewer total employees”, who
have never “received more than $3,000,000 USD in capital from an outside source”
(source: syncfusion.com/products/communitylicense, accessed 2026-09-02). 749,645
installs a month; fifteen dependencies.
Bryntum Grid (@bryntum/grid 7.3.5). The npm metadata says MIT; the vendor’s
store says “Not open source” and prices it at $680 per developer for a minimum of three
(Small Team) or $600 per developer for a minimum of ten (Large Team), perpetual with a
year of upgrades (source: bryntum.com/store/grid/, accessed 2026-09-02). The registry
field is wrong and the store is the authority. 308 installs a month; the trial is a
download, not the package.
ZingGrid (zinggrid 2.2.0). No license field on npm, no public repository (the
GitHub path returns 404). Proprietary.
AG Grid Enterprise and RevoGrid Pro are the paid tiers of two core-set libraries and are described on those pages. Handsontable is a commercial license with a non-commercial exemption, on its page.
DHTMLX, Webix, Kendo UI and FancyGrid are also commercial grids with framework-free builds; none was observed in this pass and none is characterized here.
S1: Framework-free JavaScript data grids#
The category#
A data grid is a component that takes an array of records and draws a sortable, scrollable table of them, with column headers, and usually with filtering, selection, editing, and export layered on top. The category has two halves that share a vocabulary and almost nothing else:
- Framework components —
@mui/x-data-grid,@tanstack/react-table, Glide Data Grid, PrimeVue’s DataTable. Each is a React, Vue or Angular component and cannot be mounted without that framework’s runtime. Survey 1.113 (UI Component Libraries) covers the systems those belong to. - Framework-free grids — a library you load with one
<script>tag or oneimport, hand a DOM element and an array, and get a rendered grid. It may also ship React or Vue wrappers, but the wrapper is a thin adapter over a core that draws its own DOM.
This survey is the second half. The boundary test is the one the sea pass posed and the one a server-rendered page (htmx, Alpine.js, Hotwire, a Django template) needs answered: does it draw rows on its own, from a script tag, with no bundler and no framework?
Who is in#
Twelve libraries pass the test and get a file or a section:
| library | package | in | why |
|---|---|---|---|
| Tabulator | tabulator-tables | core | the one most often named for this job |
| Grid.js | gridjs | core | the smallest full grid; bundles Preact internally |
| AG Grid Community | ag-grid-community | core | the most-installed grid on npm with a free tier |
| Handsontable | handsontable | core | the spreadsheet-shaped grid; non-commercial free tier |
| DataTables | datatables.net | core | the oldest, and it dropped jQuery in 3.0 (July 2026) |
| SlickGrid | slickgrid | core | the original virtual-scrolling grid, maintained as a fork |
| RevoGrid | @revolist/revogrid | core | a web component; the only one here built as a custom element |
| Toast UI Grid | tui-grid | core, archived | still installed monthly; the repository is archived |
| Cheetah Grid | cheetah-grid | also | canvas rendering, no row DOM |
| regular-table | regular-table | also | a FINOS virtual <table> element |
| simple-datatables | simple-datatables | also | a DataTables-shaped vanilla grid under LGPL |
| TanStack Table core | @tanstack/table-core | also, headless | fails the boundary test on purpose: it draws nothing |
Also observed, in also-in-the-category.md: Perspective, w2ui, Frappe DataTable,
canvas-datagrid, jsGrid, List.js, the React-only Glide Data Grid, and the commercial
grids (Syncfusion, Bryntum, ZingGrid) with what their license pages say.
Four rendering strategies, not one#
Every library here says it handles large data. They do it four different ways, and the strategy predicts more about a grid’s behavior than any feature list:
| strategy | what exists in the DOM for 100,000 rows | libraries |
|---|---|---|
| virtual window | the rows in view plus a buffer; scrolling swaps them | Tabulator, AG Grid, Handsontable, SlickGrid, RevoGrid, regular-table |
| paged | one page of rows; the rest is an array | DataTables (default), simple-datatables (default), Grid.js with pagination |
| full render | every row | Grid.js (default), DataTables with paging: false |
| canvas | no rows at all; pixels on one <canvas> | Cheetah Grid |
A paged grid and a virtual grid can both claim a million rows. Only one of them can show a user row 512,000 by dragging a scrollbar. The S2 measurements are organized by this table rather than by library, because a number belongs to a strategy before it belongs to a product.
What this pass checked, per library#
- Registry facts — version, release date, license, dependencies, downloads, stars,
and the stall test (open PRs against PRs merged and commits landed since 2026-03-01).
All from
observed-data.md; the harvest script and raw JSON are inharness/1-113-1-data-grids/. - The
<script>story — which file defines a global, what it is called, and what stylesheet the quick-start names. - Remote data — whether sort, filter and paging can be handed to a server, and under what option names.
- Accessibility — what the vendor claims, and what the shipped source contains
(
roleandaria-*strings), ahead of S2’s scan of the rendered DOM. - Theming — whether the shipped CSS defines custom properties, and how many.
- Advisories — the GitHub Advisory Database for the package and its dependencies.
What reading could not settle#
- Where each strategy’s row-count cliff sits on the same data.
- What a page pays in bytes for each, measured on the file it would load.
- Whether the accessibility claims survive an automated scan and a keyboard.
- Whether “themable with CSS variables” is a property of the stylesheet or of the docs.
Those are the four levels of S2-comprehensive/measurement-plan.md.
DataTables#
The oldest grid here, and as of 3.0.0 on 2026-07-24 a dependency-free one. For fourteen years “DataTables” meant “jQuery DataTables”. That is no longer a fact about the software, and most of what is written about it on the web is now describing the previous major.
Registry#
| version | 3.0.3, released 2026-08-31 |
| license | MIT |
| dependencies | none |
| downloads, last month | 2,873,800 (datatables.net core) |
| stars | 795 on DataTables/DataTablesSrc, the source repository |
| open issues / open PRs | 4 / 19 |
| PRs merged / commits since 2026-03-01 | 6 / 201 |
| releases in 12 months | 11 |
Source: observed-data.md.
3.0#
The release post, 2026-07-24: “DataTables 3 marks a significant step forward for
DataTables: It has no dependencies (no jQuery requirement any more) Core and all the
extensions are now Typescript Modernisation of the DataTables internals DataTables Plus
launches today” (source: datatables.net/blog/2026/datatables-3, accessed 2026-09-02).
The UMD wrapper in the shipped file confirms it: the browser branch assigns
window.DataTable = factory(window, document) with no jQuery argument (measured
2026-09-02; datatables.net 3.0.3; js/dataTables.js).
The stall test needs the second reading here. Six PRs merged against 201 commits is an author-driven project — SpryMedia’s Allan Jardine is its author and has been since the first release — where inbound patches are a minority input, not a queue nobody is watching. Four open issues on the source repository is the lowest count in the survey.
DataTables Plus#
Core is MIT and free. “DataTables Plus” is the commercial tier, launched with 3.0: Editor and CardView, licensed by developer count at $219 for one developer then $88 a year, $999 for five, $1,699 for ten (source: datatables.net/plus/, accessed 2026-09-02). Editor has been the paid extension since 2012; Plus is the new name for that line.
Loading it from a script tag#
js/dataTables.min.js defines window.DataTable. Styling is a separate package by
design: datatables.net-dt is the default stylesheet, and -bs5, -bm, -ft and
others integrate Bootstrap, Bulma and Foundation. The vendor’s own CDN builds one URL from
the extensions and style you pick (source: datatables.net, accessed 2026-09-02). The
package declares neither browser, unpkg nor jsdelivr.
It needs a <table> element. Every other library here takes a <div>.
Rendering#
Paged by default, ten rows a page. paging: false renders every row. Virtual scrolling
is the Scroller extension, not core. S2 measures both core configurations.
Remote data#
The most specified server protocol in the category. With serverSide: true the grid
sends draw, start, length, search[value], order[i][column],
order[i][dir] and per-column search parameters on every redraw, and expects
draw, recordsTotal, recordsFiltered and data back (source:
datatables.net/manual/server-side, accessed 2026-09-02). Server libraries exist for most
backends because the contract has been stable for a decade. Server-side processing exists
“particularly when dealing with many thousands or millions of data rows” — the same page.
Accessibility#
The manual’s accessibility page returns 404 at the time of this pass (measured
2026-09-02; datatables.net/manual/accessibility). The shipped source writes aria-sort,
aria-describedby, aria-controls, aria-live, aria-current and aria-label
(measured 2026-09-02; datatables.net 3.0.3; grep js/dataTables.js). Cell keyboard
navigation is the KeyTable extension. The rendered table is a native <table>.
Theming#
59 custom properties in datatables.net-dt’s stylesheet, --dt-* (measured 2026-09-02;
datatables.net-dt 3.0.3). The styling manual says “DataTables has full support for dark
mode in its default styling, Bootstrap 5.3+ and Bulma 1+” (source:
datatables.net/manual/styling/, accessed 2026-09-02).
Advisories#
Two, both old: CVE-2020-28458 prototype pollution (< 1.10.22) and CVE-2021-23445 XSS (< 1.11.3) (source: api.github.com/advisories, accessed 2026-09-02).
Where it sits#
The server-side grid. A page whose rows live in a database and whose list should never
be fully downloaded gets a stable request contract, a 41 KB script and a native table.
The paging default and the separate styling package are the two things a reader has to
know before the first new DataTable(...).
Grid.js#
A 16 KB grid that renders with a bundled copy of Preact and has not merged a patch in 2026. It is the lightest complete grid in the category and the one whose repository state most contradicts its download count.
Registry#
| version | 6.2.0, released 2024-03-03 |
| license | MIT |
| dependencies | preact (bundled into the UMD build) |
| downloads, last month | 177,477 |
| stars | 4,688 |
| open issues / open PRs | 62 / 32 |
| PRs merged / commits since 2026-03-01 | 0 / 0 |
| last push | 2026-01-29 |
| releases in 12 months | 0 |
Source: observed-data.md.
The stall test#
Thirty-two pull requests are open. The newest was opened 2026-05-26 (source:
api.github.com, accessed 2026-09-02). None has been merged since March and no commit has
landed since January. In the four-cell reading ADDING-RESEARCH.md prescribes — patches
arriving, none merged — this is the stalled cell, not the finished one: people are
still sending work and nobody is taking it. The last release is thirty months old.
That is a durability signal and not a quality verdict. The code is MIT, small, and the patches exist; a fork is cheap. But a reader choosing it in September 2026 is choosing a library whose next release has no visible owner.
Loading it from a script tag#
dist/gridjs.umd.js defines window.gridjs; dist/theme/mermaid.min.css is the one
shipped theme. The package declares no browser, unpkg or jsdelivr field. Its npm
name was first published in 2013 for an unrelated package, so the registry’s
first-published date says nothing about this project.
Rendering#
Every row. The word virtual does not occur in the shipped build (measured 2026-09-02;
gridjs 6.2.0; grep -c -i virtual dist/gridjs.umd.js = 0). With pagination: false
the grid draws all of its data as <tr> elements inside a scrolling wrapper; with
pagination: { limit } it draws one page. There is no in-between, and the documentation
has no page about large datasets. The 100,000-row case is therefore either a full render
or a paged one, and S2 measures both.
Remote data#
The server option takes url, method, headers, body, a then function to
select rows from the response, a handle function to process the raw response, and a
total function to report the row count for paging (source: gridjs.io/docs/config/server,
accessed 2026-09-02). Server-side sort and search are done by rewriting url from the
sort and search state in those callbacks. It is a fetch wrapper, not a protocol.
Accessibility#
The rendered table is a native <table>, which carries table semantics on its own. The
build adds role="grid" on the wrapper, aria-label, and a role="status" live region
for the loading state (measured 2026-09-02; gridjs 6.2.0; grep dist/gridjs.umd.js).
There is no cell-level keyboard navigation to look for. Two accessibility issues have
ever been filed (source: api.github.com search, accessed 2026-09-02).
Theming#
Zero custom properties in mermaid.css (measured 2026-09-02; gridjs 6.2.0). Theming is
the className option and overriding the gridjs-* classes.
Advisories#
None recorded (source: api.github.com/advisories, accessed 2026-09-02).
Where it sits#
The right size for a list that fits on a page and needs sort, search and paging with one tag. Its stall is the fact a reader has to weigh, and its full-render default is the number S2 has to show.
Handsontable#
A spreadsheet that renders as a grid, free for non-commercial use and priced per developer for everything else. It is the most starred project in the survey and the only one whose free tier is a license restriction rather than a feature line.
Registry#
| version | 18.1.0, released 2026-09-01 |
| license | “SEE LICENSE IN LICENSE.txt”: non-commercial free, commercial paid |
| dependencies | none |
| downloads, last month | 1,236,340 |
| stars | 22,033 |
| open issues / open PRs | 27 / 18 |
| PRs merged / commits since 2026-03-01 | 1,074 / 1,192 |
| npm publishes in 12 months | 676, of which 668 are prerelease-tagged dev builds; 6 stable |
Source: observed-data.md; prerelease split measured 2026-09-02 from the registry’s
time map.
License#
The shipped LICENSE.txt says the software is “dual-licensed - depending on whether
your use for commercial purposes”, that strictly personal or evaluation use is under the
non-commercial license, and that “you must not make any such use of this software as to
develop software which may be considered competitive with this software” (measured
2026-09-02; handsontable 18.1.0; LICENSE.txt). The documentation puts it more sharply:
“you can’t enter the production stage if your project is in any way connected with your
commercial activity” (source: handsontable.com/docs/javascript-data-grid/software-license/,
accessed 2026-09-02).
Commercial: from $999 per developer (Standard) and $1,299 (Priority), re-assignable,
with a 45-day trial (source: handsontable.com/pricing, accessed 2026-09-02). The
evaluation key is the string non-commercial-and-evaluation, which is what the S2
harness uses.
Cadence#
Three major versions in twelve months — 16.2.0, 17.0.0, 17.1.0, 18.0.0, 18.1.0 — on a
project that publishes a dev build to npm most working days (measured 2026-09-02; registry
time map). A reader pinning a major should expect to do it again within the year.
Loading it from a script tag#
dist/handsontable.full.min.js defines window.Handsontable and bundles every plugin;
dist/handsontable.min.js is the core without them. The package’s jsdelivr and
unpkg fields both point at the full build. Styling is two stylesheets: the
base styles/handsontable.min.css and a theme, ht-theme-main, ht-theme-horizon or
ht-theme-classic, each with a light and dark mode (source:
handsontable.com/docs/javascript-data-grid/themes/, accessed 2026-09-02).
Rendering#
Row virtualization is on by default and “can be turned off by setting the renderAllRows option to true”; scrolling cost “depends mainly on four factors” of which the first is “number of rows multiplied by the number of columns” (source: handsontable.com/docs/javascript-data-grid/row-virtualization/, accessed 2026-09-02).
Remote data#
There is no remote row model. Data is an array or an array of objects in memory; paging
and server-side sort are the application’s job, wired through loadData and the hooks.
This is a spreadsheet’s model, and it is the right one for editing a sheet and the wrong
one for a million-row report.
Accessibility#
The vendor claims WCAG 2.1 AA and tests each release with NVDA, JAWS and VoiceOver. It
also says something no other vendor here says: aria-rowcount is set to -1 on
purpose because screen readers handle it inconsistently, and “for optimal accessibility”
the page should disable virtualization with renderAllRows and renderAllColumns so the
accessibility tree is complete (source: handsontable.com/docs/javascript-data-grid/accessibility/,
accessed 2026-09-02). That is a stated trade between the virtual renderer and the screen
reader, and S3 has to carry it: a reader who needs both a large grid and a screen-reader
user cannot have both from this library’s defaults.
Theming#
336 custom properties in ht-theme-classic-no-icons.min.css, 334 in ht-theme-main.css
(measured 2026-09-02; handsontable 18.1.0). The theme is applied by a class on the
container or through a themeName option, and the documentation offers both a Theme API
and “CSS variables” as customization paths (source: themes page, above).
Advisories#
One, CVE-2021-23446, inefficient regular expression, fixed in 10.0.0 (source: api.github.com/advisories, accessed 2026-09-02).
Where it sits#
The choice when the grid is a sheet: cell editing, formulas through the HyperFormula sibling, copy-paste from Excel. For a read-mostly report on a commercial site it is a per-developer license for features the page will not use. The non-commercial tier is real and narrow: research, private study, evaluation.
Observed data — harvested 2026-09-02#
Every figure in this pass is a cell in these tables. npm registry (registry.npmjs.org, api.npmjs.org/downloads/point/last-month) and the GitHub REST and search APIs, read on the date above by harness/1-113-1-data-grids/harvest/harvest.py; observed-data.json beside it holds the raw responses.
Registry#
| package | latest | released | first published | license (npm) | deps | releases 12 mo / total | downloads last month |
|---|---|---|---|---|---|---|---|
tabulator-tables | 6.5.2 | 2026-06-23 | 2018-09-23 | MIT | 0 | 4 / 105 | 700,584 |
gridjs | 6.2.0 | 2024-03-03 | 2013-09-05 | MIT | 1 | 0 / 76 | 177,477 |
ag-grid-community | 36.1.0 | 2026-08-05 | 2018-08-21 | MIT | 2 | 14 / 143 | 12,666,018 |
handsontable | 18.1.0 | 2026-09-01 | 2016-03-04 | SEE LICENSE IN LICENSE.txt | 0 | 676 / 1794 | 1,236,340 |
datatables.net | 3.0.3 | 2026-08-31 | 2015-10-07 | MIT | 0 | 11 / 75 | 2,873,800 |
slickgrid | 5.20.0 | 2026-09-02 | 2013-02-22 | MIT | 1 | 12 / 141 | 49,891 |
tui-grid | 4.21.22 | 2024-01-10 | 2017-07-27 | MIT | 4 | 0 / 96 | 14,164 |
@revolist/revogrid | 4.27.0 | 2026-08-24 | 2020-05-04 | MIT | 0 | 59 / 646 | 133,769 |
cheetah-grid | 2.2.0 | 2026-08-20 | 2017-11-02 | MIT | 0 | 1 / 132 | 5,266 |
regular-table | 0.9.0 | 2026-08-11 | 2020-05-23 | Apache-2.0 | 0 | 14 / 59 | 74,182 |
simple-datatables | 10.3.0 | 2026-07-30 | 2018-08-17 | LGPL-3.0 | 2 | 4 / 103 | 80,225 |
@tanstack/table-core | 9.2.4 | 2026-08-28 | 2022-03-29 | MIT | 1 | 97 / 294 | 81,836,911 |
@finos/perspective-viewer | 3.8.0 | 2025-09-03 | 2019-04-30 | Apache-2.0 | 2 | 1 / 162 | 56,069 |
w2ui | 2.0.0 | 2023-04-26 | 2015-11-21 | MIT | 0 | 0 / 5 | 4,283 |
frappe-datatable | 1.20.7 | 2026-07-22 | 2018-02-20 | MIT | 3 | 9 / 105 | 300,208 |
canvas-datagrid | 0.4.7 | 2023-12-29 | 2016-11-30 | BSD-3-Clause | 1 | 0 / 208 | 27,793 |
jsgrid | 1.5.3 | 2016-12-15 | 2014-12-27 | MIT | 0 | 0 / 12 | 81,235 |
list.js | 2.3.1 | 2021-01-17 | 2014-08-30 | MIT | 1 | 0 / 11 | 216,210 |
@syncfusion/ej2-grids | 34.2.6 | 2026-09-01 | 2017-05-10 | SEE LICENSE IN license | 15 | 55 / 477 | 749,645 |
@bryntum/grid | 7.3.5 | 2026-08-21 | 2024-03-11 | MIT | 0 | 13 / 14 | 308 |
zinggrid | 2.2.0 | 2026-06-11 | 2018-10-23 | None | 0 | 1 / 49 | 624 |
@glideapps/glide-data-grid | 6.0.3 | 2024-02-03 | 2020-11-10 | MIT | 3 | 2 / 310 | 1,206,594 |
ag-grid-enterprise | 36.1.0 | 2026-08-05 | 2016-03-08 | Commercial | 2 | 14 / 243 | 5,968,698 |
Repository and the stall test (PRs merged since 2026-03-01, commits since 2026-03-01)#
| package | repo | stars | open issues | open PRs | merged | commits | last push | latest release | archived |
|---|---|---|---|---|---|---|---|---|---|
tabulator-tables | tabulator-tables/tabulator | 7,753 | 364 | 22 | 41 | 161 | 2026-09-01 | 6.5.2 2026-06-23 | no |
gridjs | grid-js/gridjs | 4,688 | 62 | 32 | 0 | 0 | 2026-01-29 | 6.2.0 2024-03-03 | no |
ag-grid-community | ag-grid/ag-grid | 15,578 | 68 | 63 | 1,509 | 1,546 | 2026-09-02 | release-36.1.0 2026-08-05 | no |
handsontable | handsontable/handsontable | 22,033 | 27 | 18 | 1,074 | 1,192 | 2026-09-02 | 18.1.0 2026-09-01 | no |
datatables.net | DataTables/DataTablesSrc | 795 | 4 | 19 | 6 | 201 | 2026-09-01 | 3.0.3 2026-08-31 | no |
slickgrid | 6pac/SlickGrid | 2,066 | 5 | 5 | 79 | 97 | 2026-09-02 | 5.20.0 2026-09-02 | no |
tui-grid | nhn/tui.grid | 2,562 | 376 | 28 | 0 | 0 | 2024-01-10 | v4.21.22 2024-01-10 | yes |
@revolist/revogrid | revolist/revogrid | 3,440 | 18 | 5 | 77 | 363 | 2026-09-01 | v4.27.0 2026-08-24 | no |
cheetah-grid | future-architect/cheetah-grid | 1,536 | 36 | 0 | 6 | 9 | 2026-08-20 | [email protected] 2026-08-20 | no |
regular-table | finos/regular-table | 403 | 19 | 0 | 8 | 24 | 2026-08-19 | — | no |
simple-datatables | fiduswriter/simple-datatables | 1,608 | — | 0 | 2 | 14 | 2026-07-30 | — | no |
@tanstack/table-core | TanStack/table | 28,398 | — | 16 | 255 | 404 | 2026-08-31 | @tanstack/[email protected] 2026-08-28 | no |
@finos/perspective-viewer | perspective-dev/perspective | 11,160 | — | — | — | — | 2026-09-01 | v5.3.0 2026-08-25 | no |
w2ui | vitmalina/w2ui | 2,740 | — | 0 | 8 | 64 | 2026-08-24 | 2.0.0 2023-04-26 | no |
frappe-datatable | frappe/datatable | 1,332 | 48 | 14 | 11 | 28 | 2026-07-22 | v1.20.7 2026-07-22 | no |
canvas-datagrid | TonyGermaneri/canvas-datagrid | 1,574 | 142 | 6 | 0 | 0 | 2025-09-18 | v0.4.7 2023-05-22 | no |
jsgrid | tabalinas/jsgrid | 1,519 | 355 | 21 | 0 | 0 | 2023-04-04 | v1.5.3 2016-12-15 | no |
list.js | javve/list.js | 11,208 | 179 | 23 | 0 | 0 | 2025-04-27 | v2.3.1 2021-01-17 | no |
@syncfusion/ej2-grids | syncfusion/ej2-javascript-ui-controls | 449 | 15 | 5 | 1 | 30 | 2026-09-01 | v34.2.6 2026-09-01 | no |
@bryntum/grid | — (no public repository located) | ||||||||
zinggrid | error: gh: Not Found (HTTP 404) | ||||||||
@glideapps/glide-data-grid | glideapps/glide-data-grid | 5,324 | 93 | 37 | 0 | 0 | 2026-01-21 | v6.0.3 2024-02-03 | no |
ag-grid-enterprise | — (no public repository located) |
Browser entry fields declared in package.json#
| package | main | module | browser / unpkg / jsdelivr | types |
|---|---|---|---|---|
tabulator-tables | dist/js/tabulator.js | dist/js/tabulator_esm.mjs | — | — |
gridjs | dist/gridjs.js | dist/gridjs.module.js | — | dist/index.d.ts |
ag-grid-community | ./dist/package/main.cjs.js | ./dist/package/main.esm.mjs | — | ./dist/types/src/main.d.ts |
handsontable | index | index.mjs | dist/handsontable.full.min.js | ./index.d.ts |
datatables.net | js/dataTables.js | js/dataTables.mjs | — | ./types/types.d.ts |
slickgrid | ./dist/cjs/index.js | ./dist/esm/index.js | — | ./dist/types/index.d.ts |
tui-grid | dist/tui-grid.js | — | — | types/index.d.ts |
@revolist/revogrid | dist/index.cjs.js | dist/index.js | dist/revo-grid/revo-grid.esm.js | dist/types/index.d.ts |
cheetah-grid | dist/main.mjs | — | dist/main.umd.js | — |
regular-table | dist/esm/regular-table.js | dist/esm/regular-table.js | dist/esm/regular-table.js | dist/esm/regular-table.d.ts |
simple-datatables | dist/index.js | dist/module.js | dist/umd/simple-datatables.js | dist/index.d.ts |
@tanstack/table-core | — | — | — | ./dist/index.d.ts |
@finos/perspective-viewer | — | — | dist/cdn/perspective-viewer.js | dist/esm/perspective-viewer.d.ts |
w2ui | w2ui-2.0.es6.min.js | — | — | — |
frappe-datatable | dist/frappe-datatable.cjs.js | — | dist/frappe-datatable.min.js | — |
canvas-datagrid | ./dist/canvas-datagrid.js | ./dist/canvas-datagrid.module.js | — | ./dist/types.d.ts |
jsgrid | dist/jsgrid.js | — | — | — |
list.js | src/index | — | — | — |
@syncfusion/ej2-grids | ./dist/ej2-grids.umd.min.js | ./index.js | — | index.d.ts |
@bryntum/grid | — | — | — | — |
zinggrid | index-commonjs.js | index.js | ZingGrid | index.d.ts |
@glideapps/glide-data-grid | dist/cjs/index.js | dist/esm/index.js | dist/esm/index.js | dist/dts/index.d.ts |
ag-grid-enterprise | ./dist/package/main.cjs.js | ./dist/package/main.esm.mjs | — | ./dist/types/src/main.d.ts |
Advisories (GitHub Advisory Database, queried 2026-09-02)#
See harness/1-113-1-data-grids/harvest/advisories.txt for the raw query output. Packages with no entry returned no advisory.
ag-grid-community: GHSA-328p-362g-r48j CVE-2024-39001 medium 2024-07-01 ag-grid packages vulnerable to Prototype Pollution | = 32.0.0 fixed:32.0.1ag-grid-community: GHSA-328p-362g-r48j CVE-2024-39001 medium 2024-07-01 ag-grid packages vulnerable to Prototype Pollution | < 31.3.4 fixed:31.3.4ag-grid-community: GHSA-876p-c77m-x2hc CVE-2024-38996 high 2024-07-01 Prototype pollution in ag-grid-community via the _.mergeDeep function | < 31.3.4 fixed:31.3.4handsontable: GHSA-hf66-r44g-p7j9 CVE-2021-23446 high 2021-09-30 Inefficient Regular Expression Complexity in handsontable | < 10.0.0 fixed:10.0.0datatables.net: GHSA-h73q-5wmj-q8pj CVE-2021-23445 medium 2021-09-29 Cross site scripting in datatables.net | < 1.11.3 fixed:1.11.3datatables.net: GHSA-m7j4-fhg6-xf5v CVE-2020-28458 high 2020-12-17 datatables.net vulnerable to Prototype Pollution due to incomplete fix | < 1.10.22 fixed:1.10.22tui-grid: GHSA-9rwj-9j2h-fhvm CVE-2022-23458 medium 2022-09-23 Toast UI Grid vulnerable to Cross-site Scripting | < 4.21.3 fixed:4.21.3
S1 recommendation#
Findings#
1. The strategy decides, and the category has four. Virtual window (Tabulator, AG Grid, Handsontable, SlickGrid, RevoGrid, regular-table), paged (DataTables, simple-datatables), full render (Grid.js by default) and canvas (Cheetah Grid). A library’s claim about “large data” is a claim about its strategy, and a paged grid’s million rows and a virtual grid’s million rows are different experiences for the person holding the scrollbar. S2 reports every number under its strategy.
2. Two of the five names in the request are not what they were. DataTables 3.0.0
(2026-07-24) has no jQuery dependency; every tutorial that says otherwise describes 2.x
(source: datatables.net/blog/2026/datatables-3, accessed 2026-09-02). Grid.js has merged
nothing since March 2026 against 32 open PRs and last released in March 2024 (source:
api.github.com, accessed 2026-09-02) — the stalled cell of the four-cell test. Toast UI
Grid, not in the request, is archived and pins a vulnerable xlsx.
3. The free tiers are three different things. MIT with no strings (Tabulator, Grid.js, DataTables core, SlickGrid, RevoGrid core, regular-table, Cheetah Grid); MIT with the reporting features held in a paid package (AG Grid: grouping, pivot, Excel, server-side row model, set filter are Enterprise; RevoGrid: server-side data and pivot are Pro); and a license that forbids commercial production use (Handsontable). LGPL (simple-datatables) is a fourth, with obligations rather than a price.
4. Server-side data is specified by two libraries and improvised by the rest.
DataTables has a fixed request and response contract with a decade of server
implementations; Tabulator names paginationMode, sortMode and filterMode as
"remote" and sends the state. AG Grid Community’s infinite model fetches ranges. Grid.js
wraps fetch. SlickGrid ships an example. Handsontable and RevoGrid’s MIT core have
nothing, and the application pages by hand.
5. Accessibility claims and accessibility source disagree in both directions. AG Grid and Handsontable claim WCAG AA and document limitations. RevoGrid claims nothing and ships the widest ARIA vocabulary in the set. Tabulator’s documentation site refused every fetch and its source sets six roles. Handsontable says in its own docs that its virtual renderer and a screen reader are at odds and recommends turning virtualization off for accessibility. None of this is a scan; S2’s is.
By situation, provisional until S2#
| situation | first look | because |
|---|---|---|
| list page over a server, rows never all downloaded | DataTables | the request contract, 41 KB, native <table> |
| client-side report with group-by, totals, CSV or XLSX download, one MIT file | Tabulator | GroupRows, ColumnCalcs and Download are in the core |
| spreadsheet editing, formulas, paste from Excel, non-commercial or licensed | Handsontable | it is a sheet; the license is the gate |
| the most capable free grid on keyboard and screen reader, reporting features not needed | AG Grid Community | the vendor’s WCAG work and issue history; 495 KB |
| the page is built from custom elements and custom properties | RevoGrid | <revo-grid>, 116 variables |
| a virtual viewport and a DataView, with the team writing the rest | SlickGrid | the original architecture, seven files |
own the <table> markup, borrow the row model | TanStack Table core | headless by design |
| a short list that fits on one page, minimum bytes | Grid.js, with the stall priced in | 16 KB, full render |
Excluded, and why#
- Toast UI Grid — archived repository, vulnerable
xlsxpin. - jsGrid, List.js, canvas-datagrid — stalled or finished; on the also-in page with their numbers.
- Glide Data Grid, MUI X, TanStack’s framework adapters, PrimeVue, Vuetify’s table — framework components; 1.113’s territory.
- Syncfusion, Bryntum, ZingGrid, DHTMLX, Kendo, Webix — commercial; license terms recorded, features not surveyed.
What S2 must settle#
The row-count cliff per strategy on one dataset; the bytes a page pays per library on
the file it loads; the axe result, ARIA roles and keyboard behavior of each rendered
grid; and whether the CSS custom properties a stylesheet defines can be set from
outside. The plan and cut line are in S2-comprehensive/measurement-plan.md.
RevoGrid#
A data grid shipped as a custom element, <revo-grid>, built with Stencil, MIT at the
core with a per-developer Pro tier. It is the only library here where “framework-free”
means “web component”: the same element mounts in plain HTML, React, Vue, Angular or
Svelte because the browser does the mounting.
Registry#
| version | 4.27.0, released 2026-08-24 |
| license | MIT (core); RevoGrid Pro is a per-developer license |
| dependencies | none |
| downloads, last month | 133,769 |
| stars | 3,440 |
| open issues / open PRs | 18 / 5 |
| PRs merged / commits since 2026-03-01 | 77 / 363 |
| releases in 12 months | 59 |
Source: observed-data.md.
Loading it from a script tag#
The unpkg field names dist/revo-grid/revo-grid.esm.js, an ES module that lazy-loads
its components as 22 further chunks from the same directory (measured 2026-09-02;
@revolist/revogrid 4.27.0; ls dist/revo-grid). So it is a <script type="module">, and
a CDN or a static host has to serve the whole directory, not one file. There is no UMD
build. Styles are inside the components; no stylesheet is loaded.
Rendering#
Virtual rows and columns. The vendor claims “1M+ rows Virtualized rendering, no jank” (vendor: rv-grid.com/pro/). Pinned rows and columns are separate viewports.
Remote data#
Not in the MIT core. The Pro page lists “Server-side grouping”, “Infinite scroll”,
“viewport” row windows and “Remote analytics” as Pro features (source: rv-grid.com/pro/,
accessed 2026-09-02); the open-source guide has no page on lazy or server data (measured
2026-09-02; grep of rv-grid.com/guide/ for server, lazy: 0 hits). A page sets
source to an array it fetched itself.
Pro#
“58 features, 13 categories” on a “per-dev license”, royalty-free, with a 30-day trial: pivot, master-detail, tree, Excel import and export, Kanban, Gantt, charts, audit trail (source: rv-grid.com/pro/, accessed 2026-09-02). No price is on the page. Grouping, filtering, sorting, editing and pinning are in the MIT core.
Accessibility#
The documentation’s accessibility URL returns 404 (measured 2026-09-02;
rv-grid.com/guide/accessibility). The shipped code writes aria-rowindex,
aria-colindex, aria-rowcount, aria-colcount, aria-selected,
aria-multiselectable, aria-keyshortcuts and aria-label (measured 2026-09-02;
@revolist/revogrid 4.27.0; grep dist/revo-grid/*.js) — the richest ARIA vocabulary in
the survey’s source, from the library with no accessibility page. Two accessibility
issues are open, seven filed in total (source: api.github.com search, accessed
2026-09-02). Whether the rendered tree validates is an S2 scan.
Theming#
116 custom properties, --revo-grid-*, and five built-in themes; “CSS variables inherit
normally. Define them on an application container to affect all descendant grids, or on
one grid for an instance-specific override” (source: rv-grid.com/guide/theme, accessed
2026-09-02). A typed ThemeDefinition is the second path.
Advisories#
None recorded (source: api.github.com/advisories, accessed 2026-09-02).
Where it sits#
The grid for a page that already thinks in custom elements and custom properties. Its open-source half is a fast client-side grid; anything that talks to a server is a Pro purchase or the application’s own code.
SlickGrid#
The grid that introduced virtual scrolling to the browser, maintained since 2013 as
the 6pac fork, now TypeScript with no jQuery. It is the smallest install base in the
core set and one of the healthiest repositories.
Registry#
| version | 5.20.0, released 2026-09-02 |
| license | MIT |
| dependencies | sortablejs (must be loaded as a global) |
| downloads, last month | 49,891 |
| stars | 2,066 |
| open issues / open PRs | 5 / 5 |
| PRs merged / commits since 2026-03-01 | 79 / 97 |
| releases in 12 months | 12 |
Source: observed-data.md.
History that changes how you load it#
Version 3 removed jQuery UI in favor of SortableJS, version 4 removed jQuery, version 5 “modernized the project by migrating to TypeScript (we kept IIFE and added ES6/ESM build targets)” and added the Alpine theme (source: github.com/6pac/SlickGrid README, accessed 2026-09-02). Older tutorials describe a jQuery plugin; the current package is not one.
Loading it from a script tag#
Seven files. Sortable.min.js from sortablejs, then slick.core.js,
slick.interactions.js, slick.grid.js and slick.dataview.js from dist/browser/,
plus slick.grid.css and a theme (slick-alpine-theme.css or the default). The
globals are Slick, Slick.Grid and Slick.Data.DataView. The README is explicit that
SortableJS “is the only hard dependency and you have to assign it yourself” (same
source). No browser, unpkg or jsdelivr field.
The DataView is where sort, filter and grouping live; the Grid is a viewport over it. This split is the architecture every later virtual grid copied.
Rendering#
Virtual rows are the whole point and have been since the original project. The grid renders the rows in view plus a buffer and recycles them on scroll.
Remote data#
slick.remotemodel.js is a worked example of a paged remote data provider, not a built-in
row model. A server-side page implements getLength and getItem over a page cache.
Compared with DataTables’ fixed request contract this is a pattern to copy.
Accessibility#
No accessibility page exists in the wiki (measured 2026-09-02; the wiki URL returns the
page-creation form). slick.grid.js sets role="grid" and aria-describedby (measured
2026-09-02; slickgrid 5.20.0; grep dist/browser/slick.grid.js). Eight accessibility
issues have been filed in the repository’s history, none open (source: api.github.com
search, accessed 2026-09-02). What the rendered DOM carries is an S2 probe.
Theming#
Three custom properties in the Alpine theme and 34 in slick-icons.css, all icon SVGs
(measured 2026-09-02; slickgrid 5.20.0). Colors and spacing are class overrides or the
SASS source.
Advisories#
None recorded (source: api.github.com/advisories, accessed 2026-09-02).
Where it sits#
A grid for a team that wants the virtual renderer and the DataView and will write the rest. Slickgrid-Universal is the same author’s framework-wrapper line for Angular, React and Vue; the vanilla package is the one this survey measures.
Tabulator#
A full-featured grid in one MIT file, with 37 modules and no dependencies, shipped as UMD and ESM. It is the library most often named for a framework-free page, and the registry agrees: 700,584 installs in the last month with no framework wrapper doing the installing (source: api.npmjs.org, accessed 2026-09-02).
Registry#
| version | 6.5.2, released 2026-06-23 |
| license | MIT |
| dependencies | none |
| downloads, last month | 700,584 |
| stars | 7,753 (repository moved to the tabulator-tables organization) |
| open issues / open PRs | 364 / 22 |
| PRs merged / commits since 2026-03-01 | 41 / 161 |
| releases in 12 months | 4 |
| types | @types/tabulator-tables 6.3.6 (DefinitelyTyped), not the package |
Source: observed-data.md.
Cadence#
6.3.0 shipped 2024-09-29, 6.3.1 on 2025-01-19, and then nothing until 6.4.0 on 2026-03-06 — a fourteen-month gap — followed by 6.5.0, 6.5.1 and 6.5.2 inside nine days in June 2026 (source: github.com/tabulator-tables/tabulator/releases, accessed 2026-09-02). Commits kept landing through the gap; it is a one-maintainer release rhythm, not a stall. The installed base is spread across that history: last week 26% of installs were 6.5.2, 19% were 6.3.1, 14% were still on 5.6.1 and 5% on 4.9.3 (source: api.npmjs.org/versions/tabulator-tables/last-week, accessed 2026-09-02).
Loading it from a script tag#
dist/js/tabulator.min.js defines window.Tabulator; dist/css/tabulator.min.css is
the base stylesheet. Twelve prebuilt stylesheets ship alongside it — Bootstrap 3, 4 and
5, Bulma, Materialize, Semantic UI, Midnight, Modern, Simple, and the site’s own light
and dark — each a complete alternative, not a layer (measured 2026-09-02;
tabulator-tables 6.5.2; ls dist/css). The package declares neither browser, unpkg
nor jsdelivr, so a CDN URL has to name the file.
Rendering#
Virtual by default. renderVertical is an option in the core defaults and the shipped
renderer is the virtual one; renderHorizontal exists for wide tables (measured
2026-09-02; tabulator-tables 6.5.2; src/js/core/defaults/options.js). What that
means for a 100,000-row array is an S2 measurement.
Remote data#
The Ajax and Page modules together cover every hand-off a server-rendered app wants:
ajaxURLandajaxRequestFuncfetch rows;ajaxParamsadds query parameters.paginationMode: "remote"sends page and size;sortMode: "remote"andfilterMode: "remote"send the sort and filter state instead of applying them locally.progressiveLoad: "scroll"or"load"fetches pages as the user scrolls or all at once.
All nine names are in the shipped source (measured 2026-09-02; tabulator-tables 6.5.2;
grep -r src/js). The documentation site refused every automated fetch during this
pass, returning its home page for every docs URL, so option semantics are from the source
and the S3 recipes are written against the source, not the site.
Group-by, calculations, export#
The GroupRows and ColumnCalcs modules do client-side grouping with per-group and table totals. The Download module writes CSV, JSON and HTML on its own, and XLSX and PDF when the page also loads SheetJS or jsPDF. The Export module copies the table to the clipboard. These are the features that make Tabulator a reporting grid rather than a list.
Accessibility#
No accessibility page was reachable (see above). The source sets role to grid,
row, gridcell, columnheader, rowgroup and columngroup, and writes aria-label,
aria-sort and aria-checked (measured 2026-09-02; tabulator-tables 6.5.2; grep -r src/js). Three accessibility issues are open against the repository and sixteen have
been filed in total (source: api.github.com search, accessed 2026-09-02). Whether a cell
takes keyboard focus is an S2 probe.
Theming#
Zero CSS custom properties in any shipped stylesheet (measured 2026-09-02;
tabulator-tables 6.5.2; grep -- '--' dist/css/*.css). Theming is SCSS variables at
build time — 163 $ variables in src/scss/tabulator.scss — or overriding classes.
An app that themes with runtime custom properties cannot reach Tabulator that way.
Advisories#
None in the GitHub Advisory Database for tabulator-tables (source:
api.github.com/advisories, accessed 2026-09-02).
Where it sits#
The default answer for a page that needs grouping, totals and download without a framework. Its costs are the ones S2 has to price: a 99 KB gzipped script, a build-time theming model, and a virtual renderer whose behavior under scroll is unmeasured here.
Toast UI Grid#
Archived. The repository nhn/tui.grid is flagged archived on GitHub and its last
push was 2024-01-10, the same day as its last release, 4.21.22 (source: api.github.com,
accessed 2026-09-02). The package still records 14,164 installs a month and 376 open
issues that will not be answered.
| version | 4.21.22, released 2024-01-10 |
| license | MIT |
| dependencies | dompurify, tui-date-picker, tui-pagination, xlsx ^0.17.1 |
| downloads, last month | 14,164 |
| stars | 2,562 |
| PRs merged / commits since 2026-03-01 | 0 / 0 |
Source: observed-data.md.
Two things a reader still using it should know:
- The
xlsxdependency is pinned to a vulnerable range.^0.17.1cannot reach 0.19.3 or 0.20.2, and the GitHub Advisory Database records CVE-2023-30533 (prototype pollution, < 0.19.3, high) and CVE-2024-22363 (regular expression denial of service, < 0.20.2, high) againstxlsx, both with no patched version on npm (source: api.github.com/advisories, accessed 2026-09-02). An archived grid cannot bump it. - The browser bundle does not include its dependencies.
dist/tui-grid.jscallsrequire("tui-date-picker"),require("tui-pagination")andrequire("xlsx")(measured 2026-09-02; tui-grid 4.21.22), so a script-tag page loads four packages, not one.
One advisory against the grid itself: CVE-2022-23458, XSS, fixed in 4.21.3 (source: api.github.com/advisories, accessed 2026-09-02).
It is not measured in S2. The reason is on this page.
S2: Comprehensive
S2: What the grids do when they run#
Four levels, planned in measurement-plan.md before any of them ran and executed on
2026-09-02 on one machine:
| level | file | rung |
|---|---|---|
L1 delivery cost — the bytes a <script> page loads, from the npm tarball | results-l1-delivery.md | measured-local |
| L2 render at 1k, 10k and 100k rows — first paint, DOM, heap, sort, scroll | results-l2-rows.md | measured-local |
| L3 accessibility — axe-core on the rendered grid, ARIA roles, keyboard focus | results-l3-accessibility.md | measured-local |
| L4 theming — set a documented custom property, look for any element that follows | results-l4-theming.md | measured-local |
feature-comparison.md is the registry-and-documentation half: what each library says
it does, laid out on one page, at rung cited.
Method#
One dataset, generated in the page from a seeded generator so every library sees byte-identical rows: eight columns — integer id, a name from a 20×20 pool, a city from twenty, an ISO date, a float amount, an integer quantity, one of six categories, a boolean. Each library gets its column definitions, a 1000×600 px container and its own defaults; where the default is paging, the paged and unpaged configurations are two rows in every table. Thirteen configurations, three sizes, three repeats, median reported and every run kept in the results file.
Chromium 151.0.7922.34 under Playwright 1.58, headless, on aarch64 Linux under WSL2.
Timers: performance.now() around each operation, then two requestAnimationFrame
calls so the number includes the paint. The floor for any figure is therefore about two
frames, 33 ms; a scroll step reported at 33 ms is a step that cost nothing. Heap is
Runtime.getHeapUsage after HeapProfiler.collectGarbage, taken after first paint and
before anything else. A run that does not finish in 60 s is reported as a timeout, not
dropped.
These figures may be compared with each other and with nothing else. The machine and browser are one of each; a vendor’s benchmark was run elsewhere on other data and does not share an axis with these.
Everything needed to re-run is in harness/1-113-1-data-grids/: the adapter pages
(one per library, a dozen lines each), the driver, the pinned tarballs, and the raw
results. uv run python run.py reproduces the sweep; --only re-runs one configuration.
Feature comparison#
What each library says about itself, read from its documentation, source, or license file on 2026-09-02. Sources are on the S1 page for each library. A cell says what is in the free package unless it says otherwise.
Rendering and data#
| Tabulator | Grid.js | AG Grid Community | Handsontable | DataTables | SlickGrid | RevoGrid | |
|---|---|---|---|---|---|---|---|
| strategy | virtual | full render, or paged | virtual | virtual | paged, or full render | virtual | virtual |
| virtual scrolling | core, default | none | core, default | core, default | Scroller extension | core, default | core, default |
| column virtualization | renderHorizontal | none | core | core | none | none | core |
| server-side sort/filter/page | remote modes + ajaxURL | server fetch wrapper | infinite row model (ranges); server-side model is Enterprise | none | serverSide protocol | pattern in slick.remotemodel.js | Pro |
| progressive load | progressiveLoad | none | infinite row model | none | Scroller extension | pattern | Pro |
| group-by | GroupRows | none | Enterprise | none (spreadsheet) | RowGroup extension | DataView grouping | core |
| column totals | ColumnCalcs | none | Enterprise (aggregation) | formulas via HyperFormula | footer callback | DataView aggregators | none |
| pivot | none | none | Enterprise | none | none | none | Pro |
Editing and export#
| Tabulator | Grid.js | AG Grid Community | Handsontable | DataTables | SlickGrid | RevoGrid | |
|---|---|---|---|---|---|---|---|
| cell editing | Edit module | plugin | core | core, the whole point | Editor (Plus, paid) | editors | core |
| CSV export | Download | none | core | ExportFile plugin | Buttons extension | none | none |
| XLSX export | Download + SheetJS | none | Enterprise | none | Buttons + SheetJS | none | Pro |
| Download + jsPDF | none | none | none | Buttons + pdfmake | none | none | |
| clipboard | Clipboard | none | Enterprise | CopyPaste plugin | Buttons | plugin | core |
| none | print layout | none | Buttons | none | none |
Integration#
| Tabulator | Grid.js | AG Grid Community | Handsontable | DataTables | SlickGrid | RevoGrid | |
|---|---|---|---|---|---|---|---|
| browser global | Tabulator | gridjs | agGrid | Handsontable | DataTable | Slick | custom element <revo-grid> |
| files to load | 2 | 2 | 1 | 3 | 2 | 7 | 1 module + 22 lazy chunks |
takes a <div> | yes | yes | yes | yes | no, a <table> | yes | the element is the grid |
| dependencies | 0 | preact (bundled) | 2 (own packages) | 0 | 0 | sortablejs (global) | 0 |
| TypeScript types | DefinitelyTyped, lagging | shipped | shipped | shipped | shipped | shipped | shipped |
| React/Vue/Angular wrappers | community | official | official | official | official | Slickgrid-Universal | official |
Accessibility, theming, license#
| Tabulator | Grid.js | AG Grid Community | Handsontable | DataTables | SlickGrid | RevoGrid | |
|---|---|---|---|---|---|---|---|
| vendor a11y claim | none reachable | none | WCAG 2.0 AA, JAWS + VoiceOver | WCAG 2.1 AA, NVDA + JAWS + VoiceOver | page 404 | none | page 404 |
| roles in source | grid, row, gridcell, columnheader, rowgroup, columngroup | grid, status, alert | grid, treegrid, row, gridcell | treegrid, row, gridcell, columnheader | native table + aria-sort | grid | rich aria-* set |
| CSS custom properties | 0 | 0 | 195 | 336 | 59 | 3 | 116 |
| theme model | 12 prebuilt stylesheets, SCSS source | one stylesheet, class overrides | Theming API objects, --ag-* | 3 themes × light/dark, --ht-* | separate style packages, dark mode | 2 themes, SASS | 5 themes, --revo-grid-*, ThemeDefinition |
| license | MIT | MIT | MIT; Enterprise EULA | non-commercial free; from $999/dev | MIT; Plus $219 then $88/yr/dev | MIT | MIT; Pro per developer |
| advisories, ever | 0 | 0 | 2 (2024, fixed) | 1 (2021, fixed) | 2 (2020–21, fixed) | 0 | 0 |
Also measured in S2, not in the tables above#
Cheetah Grid (canvas), regular-table (virtual <table> element), simple-datatables
(paged and unpaged), and TanStack Table core (headless; sized, not rendered). Toast UI
Grid is not measured; the reason is on its S1 page.
Loading recipes#
The mount code the harness used for each library, reduced to what a page needs: the
files, the global, the call. rows is an array of objects with the eight fields in
approach.md; COLUMNS is the list of field names. These are the adapter pages in
harness/1-113-1-data-grids/bench/pages/ with the measurement scaffolding removed.
Tabulator 6.5.2#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tabulator.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tabulator.min.js"></script>
<div id="grid"></div>
<script>
const table = new Tabulator("#grid", {
data: rows, height: "600px",
columns: COLUMNS.map(c => ({ title: c, field: c })),
});
table.on("tableBuilt", () => { /* ready */ });
table.setSort("amount", "desc");
table.setFilter("name", "like", "an");
// remote: { ajaxURL: "/api/items", paginationMode: "remote", sortMode: "remote", filterMode: "remote" }
</script>Grid.js 6.2.0#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/theme/mermaid.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gridjs.umd.js"></script>
<div id="grid"></div>
<script>
const grid = new gridjs.Grid({
columns: COLUMNS, data: rows.map(r => COLUMNS.map(c => r[c])),
sort: true, search: true, pagination: { limit: 50 }, // omit pagination and every row renders
}).render(document.getElementById("grid"));
// remote: server: { url: "/api/items", then: res => res.items.map(...), total: res => res.count }
</script>AG Grid Community 36.1.0#
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ag-grid-community.min.js"></script>
<div id="grid" style="height:600px"></div>
<script>
const api = agGrid.createGrid(document.getElementById("grid"), {
rowData: rows,
columnDefs: COLUMNS.map(c => ({ field: c })),
onFirstDataRendered: () => { /* ready */ },
});
api.applyColumnState({ state: [{ colId: "amount", sort: "desc" }], defaultState: { sort: null } });
api.setGridOption("quickFilterText", "an");
// no stylesheet: the Theming API injects Quartz. Legacy: theme: "legacy" + ag-grid.css + ag-theme-quartz.css
</script>Handsontable 18.1.0#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/handsontable.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/ht-theme-main.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/handsontable.full.min.js"></script>
<div id="grid" class="ht-theme-main"></div>
<script>
const hot = new Handsontable(document.getElementById("grid"), {
data: rows, columns: COLUMNS.map(c => ({ data: c })), colHeaders: COLUMNS,
height: 600, columnSorting: true, filters: true,
licenseKey: "non-commercial-and-evaluation", // runs in production too; the license text is what forbids it
});
hot.getPlugin("columnSorting").sort({ column: 4, sortOrder: "desc" });
</script>DataTables 3.0.3#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/dataTables.dataTables.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/dataTables.min.js"></script>
<table id="t" class="display" style="width:100%"></table>
<script>
const dt = new DataTable("#t", {
data: rows, columns: COLUMNS.map(c => ({ data: c, title: c })),
paging: true, scrollY: "550px",
});
dt.order([4, "desc"]).draw();
dt.search("an").draw();
// remote: { serverSide: true, ajax: "/api/items" } — the server answers draw/start/length/order/search
</script>SlickGrid 5.20.0#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/styles/css/slick.grid.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/styles/css/slick-alpine-theme.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser/slick.core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser/slick.interactions.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser/slick.grid.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/browser/slick.dataview.js"></script>
<div id="grid" class="slick-alpine-theme" style="height:600px"></div>
<script>
const dv = new Slick.Data.DataView();
const grid = new Slick.Grid("#grid", dv, COLUMNS.map(c => ({ id: c, name: c, field: c, sortable: true })), {});
dv.onRowCountChanged.subscribe(() => { grid.updateRowCount(); grid.render(); });
dv.onRowsChanged.subscribe((e, a) => { grid.invalidateRows(a.rows); grid.render(); });
dv.setItems(rows); grid.init();
dv.sort((a, b) => b.amount - a.amount); grid.invalidate();
dv.setFilter(it => it.name.includes("an")); dv.refresh();
</script>RevoGrid 4.27.0#
<script type="module" src="https://cdn.jsdelivr.net/npm/@revolist/[email protected]/dist/revo-grid/revo-grid.esm.js"></script>
<revo-grid id="rg" style="height:600px"></revo-grid>
<script>
customElements.whenDefined("revo-grid").then(() => {
const rg = document.getElementById("rg");
rg.columns = COLUMNS.map(c => ({ prop: c, name: c, sortable: true }));
rg.source = rows;
rg.updateColumnSorting({ prop: "amount", name: "amount", sortable: true }, "desc", false);
});
// the module lazy-loads 22 chunks from the same directory; a self-hosted copy needs the whole dist/revo-grid/
</script>Cheetah Grid 2.2.0#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/main.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/main.umd.js"></script>
<div id="grid" style="height:600px"></div>
<script>
const grid = new cheetahGrid.ListGrid({
parentElement: document.getElementById("grid"),
header: COLUMNS.map(c => ({ field: c, caption: c, sort: true })),
records: rows,
});
grid.dataSource.sort("amount", "desc"); grid.sortState = { col: 4, order: "desc" }; grid.invalidate();
</script>regular-table 0.9.0#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/material.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/esm/regular-table.js"></script>
<regular-table id="rt" style="height:600px;display:block"></regular-table>
<script>
customElements.whenDefined("regular-table").then(async () => {
const rt = document.getElementById("rt");
rt.setDataListener((x0, y0, x1, y1) => ({
num_rows: rows.length, num_columns: COLUMNS.length,
column_headers: COLUMNS.slice(x0, x1).map(c => [c]),
data: COLUMNS.slice(x0, x1).map(c => rows.slice(y0, y1).map(r => r[c])),
}));
await rt.draw(); // sort and filter are the page's job: change `rows`, call draw()
});
</script>simple-datatables 10.3.0#
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/style.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/simple-datatables.js"></script>
<table id="t"></table>
<script>
const dt = new simpleDatatables.DataTable("#t", {
data: { headings: COLUMNS, data: rows.map(r => COLUMNS.map(c => r[c])) },
paging: true, searchable: true, sortable: true,
});
dt.columns.sort(4, "desc"); dt.search("an");
</script>The CDN URLs above are the jsDelivr shape for the npm package at the measured version; the harness served the same files from the tarballs. Two libraries need care on a CDN: RevoGrid must be served as a directory, and SlickGrid is seven requests.
S2 measurement plan#
Written before S2, per Step 3.5 of ADDING-RESEARCH.md. Levels are ordered by cost. The
default cut line is everything that runs in one container with no external service and
no second machine; the rest is deferred and filed.
Open questions#
S1 ends with four things reading cannot settle:
- Where the row-count cliff sits. Every library says it handles “large” data. Only some virtualize the DOM, and the ones that do not (Grid.js, DataTables without paging) have never been measured against the ones that do on the same dataset.
- What a page pays to load one. Vendors quote bundle sizes for different files —
the ESM entry, the UMD build, with or without CSS. The number a
<script>tag pays is one specific file plus one specific stylesheet, gzipped. - Whether the accessibility claims survive a scan. Two vendors claim WCAG 2.x AA. Three document nothing. A scan of the rendered DOM is cheap and the same for all.
- Whether “themable with CSS variables” is true or aspirational. Counting the custom properties a stylesheet defines is a static fact, and a page can change one and watch whether the grid follows.
The structural constraint#
The category has three rendering architectures (S1 finding 1), and they cannot be made to do the same work:
| architecture | libraries | what “render 100k rows” means |
|---|---|---|
| virtual DOM window | Tabulator, AG Grid, Handsontable, SlickGrid, RevoGrid, regular-table | rows in the viewport plus a buffer exist as DOM |
| paged | DataTables (default), simple-datatables, Grid.js with pagination | one page exists as DOM; the rest is an array |
| full render | Grid.js (default), DataTables with paging: false | every row exists as DOM |
| canvas | Cheetah Grid | no row DOM at all; pixels |
So the timings are not one ranking. They are the same workload run through four different strategies, and the report says which strategy each number belongs to. A paged grid at 100k rows is measuring array handling, not rendering; a full-render grid at 100k rows is measuring the browser’s DOM ceiling, which is the finding.
The levels#
| level | settles | cost | rung | covers | |
|---|---|---|---|---|---|
| L0 | Registry facts — versions, dates, license, downloads, stars, PR stall test, advisories | what exists and what state it is in | done in S1 (S1-rapid/harvest.py) | cited | 23/23 |
| L1 | Delivery cost — bytes of the browser build and its stylesheet from the npm tarball, gzip -9; number of files a <script> page must load; CSS custom properties defined | question 2, question 4 (static half) | ~1 h | measured-local | 12/12 |
| L2 | Render at 1k / 10k / 100k rows — same 8-column dataset, seeded; time to first paint, DOM node count, JS heap, sort via API, scroll frame times; headless Chromium via Playwright | question 1 | ~half a day | measured-local | 12/12, with timeouts recorded as results |
| L3 | Accessibility scan — axe-core on the rendered grid; ARIA roles present; whether a cell takes keyboard focus | question 3 | ~2 h | measured-local | 12/12 |
| L4 | Theme probe — set one custom property on the container, read back the computed header background | question 4 (dynamic half) | ~1 h | measured-local | the ones L1 says define variables |
| L5 | Remote data — server-side sort/filter/page against a real API, request shape per library | which libraries can hand the work to a server, and what the contract is | ~1 day, needs a server process | measured-local | subset |
| L6 | Screen-reader session — NVDA or VoiceOver reading a grid | whether the axe result means anything to a user | a Windows or macOS machine, a person | measured-local | subset |
| L7 | Floor model — the reader runs L2 in their own browser | the reader can check me | a real build | measured-browser | 12/12, the page is the harness |
The cut#
S2 takes L1–L4. One container (the bench’s uv environment with Playwright’s Chromium), no external service, no second machine. That settles all four of S1’s questions at the rung this survey can claim, within a working session.
L5 and L6 are deferred and filed as a follow-up bead on re-17n. L5 is the one
that matters for the server-side persona in S3, and until it runs S3 speaks from the
documented request contracts (cited), not from a trace. L6 is where accessibility
claims are actually tested; an axe pass is necessary and not sufficient, and S2 says so.
Rules#
Same rows, same columns, default configuration. One seeded generator, eight columns (integer, string, categorical, ISO date, float, integer, categorical, boolean). Each library gets its column definitions and a 600 px tall container, and nothing else unless it cannot render without it. Where a library’s default is paging, it is measured paged AND with paging off, as two named configurations, because they are two different strategies and a reader deserves to see both cliffs.
Timeouts are results. A library that does not finish 100k rows in 60 s is reported
at 60 s with a timeout flag, not dropped from the table.
Numbers are comparable with each other and with nothing else. aarch64 under WSL2, one headless Chromium build, one machine. A vendor’s “renders a million rows” was measured on other hardware with another workload and does not share an axis with these.
Where the floor model comes from#
L2 is a browser workload by construction; the harness pages are the floor model, minus the chrome. This survey is JavaScript, so Pyodide is not involved and the shared Workshop library’s Python loader does not apply. L7 is filed, not built, and it is not gated on L5.
S2 recommendation#
What the four levels settled#
The row-count cliff is a strategy property, and it sits between 10,000 and 100,000 rows for full render. Every full-render configuration finished 10,000 rows and no full-render configuration finished 100,000: Grid.js timed out at 60 s, and DataTables and simple-datatables with paging off blocked the page’s own load event past 30 s (measured 2026-09-02; L2). At 10,000 rows the same configurations already cost 1.8–4.6 s to first paint and 90,000 DOM nodes. Every virtual configuration held its DOM node count flat across all three sizes — Tabulator at 1,082, AG Grid at 404, SlickGrid at 305 — and painted 100,000 rows in 195–224 ms (Tabulator, AG Grid, RevoGrid) or 46 ms (SlickGrid). Handsontable is the virtual grid that scales worst on first paint, 1,119 ms at 100k, and DataTables’ paged mode pays 1,061 ms to index 100,000 rows it will show ten of.
A page pays between 19 KB and 495 KB for the same job. L1’s table, from the file a script tag loads. AG Grid’s 495 KB corrects the 362 KB figure in the pass that filed this survey; that number was the bundler entry.
Two libraries pass the axe scan with a full ARIA grid; one passes with none. AG Grid
Community renders role="grid" with 378 gridcells, aria-rowcount, 458 focusable
elements and zero violations, and is the only library where a real click followed by
ArrowDown moved DOM focus to the next row. Handsontable moves focus too, on a native
<table> with treegrid, and carries one critical and one serious violation. Grid.js
and regular-table pass the scan because they render native tables with almost no ARIA
to get wrong. Tabulator, SlickGrid and RevoGrid put focus inside the grid on click and
do not move it on ArrowDown — cell navigation is a module, a plugin, or absent. RevoGrid
has the most violations here (five rules, 52 nodes) despite the widest ARIA vocabulary in
its source: the attributes are present and the values and parents are wrong. Cheetah
Grid’s canvas has no cell to click (measured 2026-09-02; L3).
“Themable with CSS variables” is true for four libraries and true from the container for three. AG Grid, RevoGrid and DataTables follow a property set on the container. Handsontable’s 336 properties do nothing from the container and fourteen elements follow when the property is set on the grid’s own root, because its theme stylesheet re-declares every variable there. Tabulator, SlickGrid, Grid.js, simple-datatables, regular-table and Cheetah Grid define no color property at all (measured 2026-09-02; L4).
Reading the numbers by what a page will do#
| the page will | read this row | and the finding is |
|---|---|---|
| show a list the server pages | DataTables paged: 61 ms at 1k, 238 nodes, 33 ms sort | paging is free below 10k; at 100k the 1,061 ms is the client-side index the server would replace |
| scroll a report | the virtual rows’ worst scroll step | Tabulator’s worst step is 280 ms at 10k and 122 ms at 100k; RevoGrid, regular-table and Cheetah never exceed 45 ms; AG Grid 229 ms at 100k |
| sort 100k client-side | the 100k sort column | SlickGrid 232 ms, Tabulator 527, AG Grid 689, Handsontable 1,200; DataTables paged 199 |
| hold 100k rows | the heap column | the data costs about 10–13 MB; SlickGrid, RevoGrid, regular-table and Cheetah add 3–5 MB over it, Tabulator 20 MB, AG Grid 26 MB, DataTables paged 50 MB, simple-datatables paged 150 MB |
| be audited | the axe and keyboard tables | AG Grid, then Handsontable; native-table grids for small data |
| follow a dark-mode toggle | L4 | AG Grid, RevoGrid, DataTables from the container; Handsontable from its root |
Corrections to S1#
- S1 called Tabulator “the default answer for a page that needs grouping, totals and download”. L2 adds the cost: the slowest virtual scroll here (280 ms worst step at 10k) and a 1,082-node DOM, three times its neighbors’. Still the answer for that job; not the fastest grid.
- S1 read SlickGrid’s small install base as a risk. L2 makes it the fastest virtual grid on first paint (46 ms at 100k), sort (232 ms) and heap (16 MB), which is the architecture doing what it was designed for.
- S1 recorded Grid.js’s
serveroption and stall. L2 adds that its default mode is a full render that paints 1,000 rows in 600 ms and 10,000 in 4.5 s, and that itspaginationmode is what makes it usable past a few hundred.
What is still open#
L5 — a real server behind the three remote-data designs, so the request shapes are
traced rather than read. L6 — a screen reader on AG Grid, Handsontable and DataTables,
because an axe pass is a floor. Both filed on re-17n. The harness pages are the floor
model minus the chrome and can become one without waiting for either.
L1: What a page pays to load each grid#
Measured 2026-09-02 from the npm tarballs at the versions in S1, gzip -9 of each file
the library’s own quick-start tells a <script> page to load; custom properties are
distinct --name: definitions in the loaded stylesheet, and in any stylesheet the
package ships (harness/1-113-1-data-grids/bench/l1_sizes.py).
| library | files a page loads | JS gz | CSS gz | total gz | custom properties loaded / in package |
|---|---|---|---|---|---|
| TanStack Table core | 1 (headless re-export; the size that matters is what the page bundles) | 1 KB | — | 1 KB | 0 / 0 |
| regular-table | 2 | 10 KB | 0.8 KB | 10 KB | 0 / 4 |
| Grid.js | 2 | 16 KB | 2.4 KB | 19 KB | 0 / 0 |
| simple-datatables | 2 | 27 KB | 1.1 KB | 28 KB | 0 / 0 |
DataTables + datatables.net-dt | 2 | 41 KB | 3.8 KB | 44 KB | 59 / 59 |
| Cheetah Grid | 2 | 84 KB | 1.7 KB | 86 KB | 6 / 6 |
| Tabulator | 2 | 99 KB | 3.8 KB | 102 KB | 0 / 0 |
| SlickGrid + SortableJS | 7 | 108 KB | 5.6 KB | 114 KB | 3 / 34 (icons) |
| Toast UI Grid | 2, plus three externals it does not bundle | 101 KB | 13.0 KB | 114 KB | 0 / 0 |
| RevoGrid | 1 module + 22 lazy chunks (sum of all; a page loads a subset) | 200 KB | in JS | 200 KB | 0 / 116 |
Handsontable (full build + base + ht-theme-main) | 3 | 439 KB | 23.1 KB | 462 KB | 336 / 336 |
| AG Grid Community | 1 (Theming API injects CSS) | 495 KB | — | 495 KB | 0 / 195 |
Reading it#
The spread is fifty to one. From Grid.js at 19 KB to AG Grid at 495 KB for the same job description. A page whose whole weight is under 100 KB doubles or sextuples by adding one of the bottom three.
The sea rig’s rapid pass had two of these wrong. It listed AG Grid Community at
362 KB gzipped and Tabulator at 102 KB. Tabulator matches. AG Grid’s UMD build is 495 KB
by gzip -9 of the file itself; the 356 KB figure belongs to dist/package/main.cjs.min.js,
the CommonJS entry a bundler would consume, not the file a script tag loads (measured
2026-09-02; ag-grid-community 36.1.0). Which file is being measured is the whole
difference, and vendor “bundle size” claims rarely say.
RevoGrid’s number is an upper bound. Stencil lazy-loads components; the 200 KB is every chunk in the directory. What one page transfers depends on which features it touches, and the S2 harness did not instrument the network. It is also the one library here that cannot be served as a single file.
SlickGrid is seven requests. Or one, after the page concatenates them; the package does not.
Custom properties are a stylesheet property, not a documentation one. Handsontable, AG Grid, RevoGrid and DataTables define them; Tabulator, Grid.js, SlickGrid (colors) and simple-datatables do not, whatever their theming pages say about customization. L4 tests whether the ones that exist can be set from outside.
S3: Need-Driven
S3: Who needs a framework-free grid#
| situation | what eliminates options | file |
|---|---|---|
| A server-rendered list page — Django, Rails, PHP, Go, or htmx + Alpine — with no bundler, whose rows live in a database | anything that needs the whole table in the browser; anything without a script-tag build | use-case-server-list.md |
| A client-side report: group by a column, subtotal, download as CSV or XLSX, from one file | grids whose grouping and export are in a paid tier; grids that page instead of scroll | use-case-client-report.md |
| A public-sector or regulated page that must pass a WCAG audit with a screen reader | grids with no accessibility work; canvas rendering; virtual renderers, conditionally | use-case-accessible.md |
| A data-entry sheet: typing into cells, pasting from Excel, formulas | everything that is a grid rather than a spreadsheet; the non-commercial license, conditionally | use-case-spreadsheet.md |
| An internal tool holding a hundred thousand rows client-side — a log viewer, a blotter, a queue | full-render and paged strategies | use-case-large-client.md |
| A design-system page with a runtime theme and a dark-mode toggle | stylesheets with no custom properties | use-case-themed.md |
| A short list on a content page: sort, search, forty rows, the fewest bytes | everything over about 50 KB; stalled projects, conditionally | use-case-small-list.md |
Each file names who has the problem, why the generic answer fails them, what they need, and which two or three libraries survive their constraints — with the S2 measurement that decides between them where one does. A situation is not a persona for one company; it is the shape of the job, and the same shape recurs across products.
S3 recommendation#
Which situation, which grid#
| situation | first choice | second | not this |
|---|---|---|---|
| server-rendered list, rows in a database | DataTables | Tabulator (remote modes) | any grid that wants the array |
| client-side report with group-by, totals, export | Tabulator | SlickGrid DataView | AG Grid Community (grouping is Enterprise) |
| must pass an accessibility audit | AG Grid Community; a native <table> grid for small data | DataTables + KeyTable | Cheetah Grid; any virtual grid without stated ARIA work |
| a spreadsheet | Handsontable, license permitting | RevoGrid | grids with cell editors |
| a hundred thousand rows client-side | a virtual grid from the L2 table | Cheetah Grid if the tree does not matter | full-render and paged configurations |
| runtime theme, dark mode | RevoGrid, AG Grid | Handsontable, DataTables | Tabulator, Grid.js, SlickGrid |
| a short list on a content page | DataTables | Grid.js, stall priced in | any grid over 100 KB |
Two things every situation shares#
The strategy question comes before the library question. Should the browser hold every row? If not, the server-side contract decides and DataTables and Tabulator are the list. If so, should the user scroll through all of them? If so, a virtual grid. If not, paging, and the smallest library that pages.
The free tier is a different thing at each vendor. Before the column definitions are written, read the line: MIT throughout (Tabulator, DataTables core, SlickGrid, Grid.js, regular-table, Cheetah Grid), MIT core with reporting features paid (AG Grid, RevoGrid), non-commercial only (Handsontable), LGPL (simple-datatables).
Conditions that flip a row#
- Accessibility plus scale flips “virtual grid” to “paged grid, or AG Grid with the limitations disclosed”.
- Formulas plus commercial use flips “spreadsheet” to “a purchase”.
- A pivot flips everything to a paid tier or to Perspective.
- A stall at the vendor flips “smallest library” to “second-smallest maintained library” — which today moves a small list from Grid.js to DataTables.
A page that has to pass an accessibility audit#
Who#
Public sector, education, healthcare, finance: any page where WCAG 2.1 AA is a contract term or a law, and where an auditor will open it with NVDA or VoiceOver and try to reach row forty with the keyboard. Also any team that has decided that is the standard regardless.
Why the generic answer fails#
Most grids are built from <div> elements. A <div> has no semantics; the library has
to write role="grid", row, columnheader and gridcell, keep aria-rowcount
accurate, move DOM focus with the arrow keys and announce sort changes. Some libraries do
all of it and say so; some do part of it and say nothing; one renders to a canvas and
has no tree at all.
And the strategy that makes large grids fast is the one that makes them hard to read aloud: a virtual renderer’s accessibility tree holds thirty rows. One vendor says so in its own documentation.
Requirements#
role="grid"or a native<table>, with rows and cells that carry roles.- Keyboard: focus enters the grid, arrow keys move the current cell, sort is reachable.
- An axe-core scan of the rendered grid with no critical violations, as the floor — and a person with a screen reader as the ceiling, which S2 could not do.
- A stated conformance claim from the vendor, because the auditor will ask.
What survives#
AG Grid Community claims WCAG 2.0 AA and Section 508, sets role="grid" or
treegrid with row and gridcell, and tests with JAWS and VoiceOver; it lists its
own limitations (source: ag-grid.com/javascript-data-grid/accessibility/, accessed
2026-09-02). In S2’s keyboard probe it is one of two libraries where a real click puts
focus on a cell and ArrowDown moves it to the next row.
Handsontable claims WCAG 2.1 AA, tests with NVDA, JAWS and VoiceOver, and moves
focus by keyboard in the probe. Its documentation recommends renderAllRows and
renderAllColumns for accessibility, turning off virtualization (source:
handsontable.com/docs/javascript-data-grid/accessibility/, accessed 2026-09-02) — which
makes a large grid a full render. And the license excludes commercial production, which
rules it out for most of the organizations in this situation.
DataTables renders a native <table>, writes aria-sort on headers and a live
region, and gets cell navigation from the KeyTable extension. Native tables are the
oldest accessible grid there is; the paged default keeps the tree complete. Its
accessibility manual page was a 404 at the time of this pass.
RevoGrid ships the widest ARIA vocabulary in the source and no accessibility page; S2’s scan is the only evidence either way, and its result is on the L3 page.
Cheetah Grid is a canvas. No cells, no roles, no text in the tree. Not for this situation unless the library’s own accessibility layer is verified with a screen reader.
Conditions#
- An axe pass is necessary and not sufficient. S2’s L3 table records violations by rule
and impact: AG Grid, Grid.js and regular-table have none; Tabulator, SlickGrid,
Handsontable and DataTables each carry a
criticalonaria-required-children, which means agridorrowrole is missing the children the role requires; RevoGrid carries five rules across 52 nodes (measured 2026-09-02;../S2-comprehensive/results-l3-accessibility.md). - If the grid is small, prefer the native
<table>path (DataTables, regular-table, Grid.js) over<div>grids: the browser supplies the semantics. - If the grid is large AND must be accessible, the trade is explicit: a paged grid (complete tree, slow to reach row 50,000) or a virtual grid whose vendor has done the ARIA work (AG Grid) and whose limitations the auditor is told about in advance.
A client-side report: group, subtotal, download#
Who#
Anyone whose page has a few thousand rows in hand — a finance close, a survey export, a project’s task list, sales by region — and needs the reader to group them by a column, see totals per group and overall, and take the result away as CSV or a spreadsheet. Often the same page as the server list, one tab over.
Why the generic answer fails#
The features that make a report are exactly the ones two vendors sell separately. Row grouping, aggregation, pivot and Excel export are AG Grid Enterprise; pivot and Excel are RevoGrid Pro; DataTables’ RowGroup and Buttons are free extensions but Editor is Plus. A reader who picks “the most popular grid” and then adds a group-by button finds a watermark.
Requirements#
- Group by one or more columns, client-side, with per-group and grand totals.
- Export the grouped view as CSV; XLSX if the reader’s users open it in Excel.
- One MIT file, no license key, no per-developer count.
- Virtual scrolling, so a ten-thousand-row report is one scrollable table and not two hundred pages.
What survives#
Tabulator is the one library here with grouping (GroupRows), column calculations
(ColumnCalcs: sum, avg, min, max, count, and custom) and download (CSV, JSON, HTML on its
own; XLSX with SheetJS, PDF with jsPDF) all in the MIT core (measured 2026-09-02;
tabulator-tables 6.5.2; src/js/modules/). That combination is why it is the default
answer for this situation.
SlickGrid’s DataView does grouping with aggregators — sum, avg, min, max, count — and the grid renders group headers and totals rows; export is the application’s job or a plugin. Seven files and a hands-on integration.
DataTables with the RowGroup and Buttons extensions groups and exports; the
paging default makes a report a set of pages unless Scroller is added too. Three
extensions to assemble against Tabulator’s one file.
AG Grid Community cannot group. RevoGrid’s core groups but does not total or export. Handsontable is a spreadsheet: it sums with formulas, not with a group-by, and the license forbids a commercial report page anyway.
Conditions#
- Over about ten thousand rows the grouped view’s cost is the group computation plus
the render. Ungrouped, Tabulator paints 10,000 rows in 116 ms and sorts them in 83 ms,
and its worst scroll step is 280 ms — the slowest scroll among the virtual grids
(measured 2026-09-02;
../S2-comprehensive/results-l2-rows.md). A grouped report is scrolled, so that is the figure to read; SlickGrid’s DataView grouping scrolls at 111 ms worst and is the alternative when the scroll matters more than the one-file setup. - If the reader’s users need a pivot — dimensions dragged into rows and columns — nothing on the free side does it. The choices are AG Grid Enterprise, RevoGrid Pro, or Perspective, which is a different kind of tool.
A hundred thousand rows in the browser#
Who#
An internal tool whose users want the whole dataset under one scrollbar: a log viewer, a trade blotter, a job queue, a metrics table, a genome annotation browser. The rows fit in memory — a hundred thousand records of eight fields is a few tens of megabytes of JavaScript objects — and the users sort and filter constantly and will not tolerate a round trip per click.
Why the generic answer fails#
The generic answer is any grid that “handles large datasets”, and the phrase means four
different things. A full-render grid builds a hundred thousand <tr>s and the tab
stalls. A paged grid shows fifty of them and turns “scroll to the middle” into “go to
page 1,000”. Only a virtual window or a canvas gives a user a scrollbar that spans the data. S2’s
L2 table exists to show where each strategy stops.
Requirements#
- Virtual rows (and columns, if the table is wide).
- First paint under a second at 100k rows; sort under a second; scroll steps that do not drop frames.
- Heap that scales with the data, not with the DOM.
- Client-side sort and filter over the whole array.
What survives#
The virtual grids — Tabulator, AG Grid Community, Handsontable, SlickGrid, RevoGrid,
regular-table — and the canvas grid, Cheetah Grid. At 100,000 rows on the same
data (measured 2026-09-02; ../S2-comprehensive/results-l2-rows.md):
| first paint | sort | worst scroll step | heap | |
|---|---|---|---|---|
| SlickGrid | 46 ms | 232 ms | 79 ms | 16 MB |
| Tabulator | 195 ms | 527 ms | 122 ms | 32 MB |
| AG Grid Community | 212 ms | 689 ms | 229 ms | 39 MB |
| Handsontable | 1,119 ms | 1,200 ms | 155 ms | 19 MB |
| regular-table | 22 ms | 66 ms (array sort + draw) | 36 ms | 13 MB |
SlickGrid is the fastest grid with a sort and filter API on every column. regular-table is faster still and has no API: the page sorts the array. Tabulator and AG Grid are within a factor of three on everything and AG Grid is the one with keyboard cell navigation. Handsontable’s first paint is five times Tabulator’s.
Grid.js without paging and DataTables with paging: false are the full-render
rows in the same table: neither finished 100,000 rows, and at 10,000 they took 4.5 s and
2.7 s to first paint with 90,000 DOM nodes. Those are the numbers this situation exists
to avoid.
Handsontable is virtual and fast and its license excludes a commercial internal tool.
Conditions#
- If the users need the accessibility tree complete, this situation and
use-case-accessible.mdconflict; the L3 page and the Handsontable documentation both say so. Decide which one wins before choosing. - If the dataset will grow past what a tab should hold — a million rows, a year of logs —
the client-side premise fails and the situation becomes
use-case-server-list.mdwith a virtual grid on the front: Tabulator’sprogressiveLoad, AG Grid’s infinite row model, or DataTables with Scroller andserverSide. - Wide tables (dozens of columns) need column virtualization too; Tabulator
(
renderHorizontal), AG Grid, Handsontable and RevoGrid have it; SlickGrid and DataTables do not.
A server-rendered list page over a database#
Who#
A team whose application renders HTML on the server and adds behavior with a script
tag: Django or Rails or Laravel with a sprinkle of htmx and Alpine.js, a Go service with
templates, a Flask admin. There is no bundler and no component tree. The page already
has /items?sort=name&page=3 routes that return rows, because that is how the site
worked before anyone said “grid”.
Why the generic answer fails#
The generic answer is “load a grid and hand it the array”. The array is a database table with two hundred thousand rows behind an ORM. Pulling it into the browser to show fifty rows turns every page view into a bulk export, and the server already knows how to sort, filter and page — the grid’s headline features are redundant with the API.
What the page needs is a grid that sends its state to the URL the server already has, or one whose request shape the server can be taught in an afternoon.
Requirements#
- Loads from one or two
<script>and<link>tags off a CDN; defines a global. - Sort, filter and paging state go to a server; the browser holds one page.
- A documented request and response contract, or at least callbacks where the page builds the URL.
- Survives an htmx swap: the grid is mounted after the fragment arrives and destroyed before it leaves, or the fragment IS the table and the grid enhances it.
- Small enough that a page which does not need it does not pay for it.
What survives#
DataTables is built for this. serverSide: true and an ajax URL send draw,
start, length, search[value] and order[i][column]/order[i][dir] on every
redraw, and expect draw, recordsTotal, recordsFiltered and data back (source:
datatables.net/manual/server-side, accessed 2026-09-02). Server-side libraries for the
contract exist for Django, Rails, Laravel, Express, .NET and more because it has not
changed in a decade. The grid enhances an existing <table>, which is the htmx-friendly
shape: the server can render the first page as HTML and DataTables takes it from there.
44 KB gzipped for script and stylesheet (measured 2026-09-02; datatables.net 3.0.3 +
datatables.net-dt 3.0.3). No jQuery since 3.0.0.
Tabulator does it with ajaxURL, paginationMode: "remote", sortMode: "remote"
and filterMode: "remote" (measured 2026-09-02; tabulator-tables 6.5.2; source); the
server receives page, size, sort[] and filter[] parameters and returns
last_page plus data. It is a <div> grid, so an htmx page mounts it in a
callback after the swap. 102 KB gzipped.
AG Grid Community’s infinite row model asks for row ranges by index with the sort and filter model attached and is the third option, at 495 KB — five times DataTables’ weight for the same job. The server-side row model with grouping is Enterprise.
Grid.js’s server option is a fetch wrapper where the page rewrites url from the
sort and search state; it works and is 19 KB, and the project has merged nothing since
March 2026 (see ../S4-strategic/gridjs-viability.md).
Conditions#
- If the rows will never exceed what one request can carry — a few thousand — the server contract is optional and any client-side grid on this list works with the page fetching once.
- If the page already paginates server-side and the “grid” only needs to sort the fifty
rows on screen, a
<table>with a fifteen-line sort script is the right library. The S2 numbers for 1,000 rows are the ones to read, and they are all fast.
A short list on a content page#
Who#
A documentation site, a marketing page, a club’s fixture list, a changelog: forty to a few hundred rows in a table that would be nicer if the headers sorted and there was a search box. No build. Often no developer after launch.
Why the generic answer fails#
The generic answer is a real grid, and a real grid is 100–500 KB of JavaScript to sort forty rows. The page’s whole weight was less than that. Full-render is the right strategy here — every row on the page is what the reader expects — and the libraries built for scale are paying for machinery this page will never use.
Requirements#
- One script and one stylesheet, as small as possible, from a CDN.
- Sort on header click, a search box, optional paging.
- Enhances an existing
<table>rather than replacing it with<div>s, so the page works without JavaScript and stays accessible. - A project that will still exist when the page is next touched.
What survives#
Grid.js at 19 KB gzipped is the smallest complete grid here, with sort, search and
paging in the core (measured 2026-09-02; gridjs 6.2.0 + mermaid.min.css). It builds its
own <table> rather than enhancing one. The repository has merged nothing since March
2026 and last released in March 2024; for a page that is set once and left, that is a
smaller risk than for a product, and it is still the fact to know
(../S4-strategic/gridjs-viability.md).
DataTables at 44 KB enhances the <table> already on the page, sorts, searches and
pages, and is maintained; the extra 25 KB buys the native-table semantics and a live
project. Since 3.0.0 it needs no jQuery.
simple-datatables at 28 KB enhances a <table> too, and is LGPL-3.0, which on a
content page is a license notice most sites would rather not think about.
A <table> and a twenty-line sort function is also a library, and for forty rows it is
often the right one.
Conditions#
- Over a few thousand rows the full-render strategy starts to cost: at 1,000 rows
Grid.js paints in 600 ms and DataTables with paging off in 300 ms; at 10,000 they take
4.5 s and 2.7 s (measured 2026-09-02;
../S2-comprehensive/results-l2-rows.md). Turn paging on, or move touse-case-large-client.md. - If the page must be accessible, prefer the
<table>-enhancing libraries; the native element carries the semantics that a<div>grid has to reconstruct.
A data-entry sheet#
Who#
A team replacing a shared spreadsheet with a page: a budget template, a roster, a price list, a lab’s sample log. The users type into cells, tab across, paste a block from Excel, expect a formula to recompute, and expect undo. They do not think of it as a grid. They think of it as the sheet, on a web page.
Why the generic answer fails#
A grid edits one cell at a time through an editor widget. A spreadsheet edits a range: type, Tab, type, Enter, paste twenty rows, fill down, Ctrl-Z. The keyboard model, the selection model and the clipboard model are different components, and grids that add “editing” have added the first of the three.
Requirements#
- Cell-by-cell keyboard entry with Tab and Enter semantics from Excel.
- Range selection, copy and paste of rectangular blocks, fill handle.
- Undo and redo across edits.
- Validation per cell; formulas if the sheet has them.
- Either a license that allows the team’s use, or a license they are willing to buy.
What survives#
Handsontable is the spreadsheet. Range selection, clipboard, fill, undo, validators and cell types are core; formulas are its HyperFormula sibling. It moves DOM focus by keyboard in S2’s probe and claims WCAG 2.1 AA. The license is the whole decision: free for “personal, exploratory projects” and research, evaluation and private study; “can’t be used in commercial settings or for commercially driven work”; from $999 per developer otherwise, re-assignable, 45-day trial (source: handsontable.com/pricing and /docs/javascript-data-grid/software-license/, accessed 2026-09-02). For a university lab or a personal project it is free; for a company’s internal budget page it is a purchase.
RevoGrid has range selection, editing, clipboard and autofill in the MIT core and markets the Pro tier as a replacement for “Handsontable, Luckysheet, HyperFormula glue” (source: rv-grid.com/pro/, accessed 2026-09-02). Formulas are Pro. For a commercial sheet without formulas it is the MIT answer.
AG Grid Community edits cells with editors and validation; range selection and clipboard are Enterprise, which is where the spreadsheet feel lives.
Tabulator’s Edit, Validate, History (undo) and Clipboard modules cover single-cell entry and paste; the SelectRange module adds range selection in 6.x. It is a grid that edits rather than a sheet, and for a form-like table with a few editable columns it is enough.
Conditions#
- If the users need formulas and the project is commercial, the choice is a Handsontable license or RevoGrid Pro; there is no free formula grid on this list.
- If the “sheet” is really a table with one or two editable columns and a save button, Tabulator or AG Grid Community, and the spreadsheet requirements above do not apply.
- The license check for Handsontable is a sentence, not a feature flag: the
non-commercial-and-evaluationkey runs in production with no watermark, and the license text is what forbids it.
A page with a runtime theme#
Who#
A product with a design system: tokens for color, spacing and type as CSS custom properties, a dark mode the user toggles, brand themes per tenant. The grid has to look like the rest of the page, follow the toggle without a reload, and not ship a second color palette.
Why the generic answer fails#
Most grid stylesheets are compiled: SCSS variables resolved at build time into fixed
colors. Restyling means overriding dozens of class selectors with !important, or
rebuilding the stylesheet from source with the project’s values, and neither follows a
runtime toggle. A grid whose stylesheet is written in custom properties can be themed
by setting those properties on an ancestor, which is what the design system already
does for everything else.
Requirements#
- Colors, borders, fonts and spacing exposed as custom properties.
- Properties that cascade from an ancestor, so one
:rootor[data-theme]rule themes every grid. - A shipped dark mode, or the properties to make one.
What survives#
S2’s L1 count of custom properties defined, and its L4 probe of whether setting one on the container changes anything, are the evidence. From L1 (measured 2026-09-02; each package’s shipped CSS):
| library | custom properties | shipped dark mode |
|---|---|---|
| Handsontable | 336 | yes, every theme |
| AG Grid Community | 195 (--ag-*), Theming API parameters | yes, via theme parameters |
| RevoGrid | 116 (--revo-grid-*) | yes, highContrastDark and others |
DataTables (-dt package) | 59 (--dt-*) | yes |
| SlickGrid | 3 in the Alpine theme, icons only | no |
| Tabulator | 0; 12 prebuilt stylesheets, 163 SCSS variables at build | tabulator_midnight and _site_dark as separate stylesheets |
| Grid.js | 0 | no |
RevoGrid documents the cascade the situation wants: “Define them on an application container to affect all descendant grids, or on one grid for an instance-specific override” (source: rv-grid.com/guide/theme, accessed 2026-09-02), and the L4 probe confirms a property set on the container reaches the header.
AG Grid exposes the same names through the Theming API and the legacy CSS; the L4 probe confirms the container cascade.
Handsontable has the most properties and the L4 probe found that setting one on the
container does nothing, because the theme stylesheet re-declares every property on the
grid’s own root element; set on that element, fourteen elements followed (measured
2026-09-02; handsontable 18.1.0; debug_hot.py). Themable, but not from :root.
DataTables has 59 properties and a documented dark mode for its default stylesheet.
Tabulator is themable at build time or by class override, and a project that already compiles SCSS can feed it tokens; a project that themes at runtime cannot reach it.
Conditions#
- If the design system is SCSS-compiled rather than custom-property-based, Tabulator’s model is the same as the project’s and the table above inverts.
- If the theme is one dark mode and nothing else, every library with a shipped dark stylesheet qualifies; the custom-property count matters when the theme is tenant- or user-specific.
S4: Strategic
AG Grid: the durability of an MIT core with a commercial line through it#
The numbers#
12,666,018 Community installs a month, 5,968,698 Enterprise; 1,509 PRs merged and
1,546 commits since March; 14 releases in twelve months; 68 open issues (source:
observed-data.md, accessed 2026-09-02). By every maintenance measure the strongest
project in the survey.
The line, and where it moved#
Enterprise is a separate package, ag-grid-enterprise, licensed per developer per
deployment, with a watermark and a console error when unlicensed (source:
ag-grid.com/javascript-data-grid/licensing/, accessed 2026-09-02). The features on the
paid side are the reporting features: row grouping, aggregation, pivot, Excel export,
set filter, server-side row model, master/detail, range selection, clipboard (source:
ag-grid.com/javascript-data-grid/community-vs-enterprise/, accessed 2026-09-02).
Enterprise installs run at 47% of Community’s. That ratio is the business, and it is what keeps the MIT core funded — and it is also the pressure that decides which side of the line a new feature lands on.
What a Community user is exposed to#
- Feature-line risk, not license risk. The MIT grant on shipped Community versions is irrevocable. The exposure is that the next feature a page needs is Enterprise, and that the Community version of an existing feature is not improved further.
- Version churn. Fourteen releases a year and a major version each year (v36 in August 2026). The Theming API introduced in v33 replaced the CSS-file model; the old path still works but is documented as legacy (source: ag-grid.com/javascript-data-grid/theming/, accessed 2026-09-02).
- Two 2024 advisories, prototype pollution, fixed within the same cycle (source: api.github.com/advisories, accessed 2026-09-02). The response time is the signal.
- Bundle weight. 495 KB gzipped for the UMD build (measured 2026-09-02; ag-grid-community 36.1.0; gzip -9). It does not get smaller with age.
Verdict#
The safest maintenance bet in the category and the most expensive in bytes. For a Community-only page, the question is whether the features it needs are on the free side today and are likely to stay there. The vendor’s own list answers the first half; the 47% ratio is the answer to the second.
S4: Viability#
Which of these grids will still be the right choice in three years, and what a reader
is exposed to if it is not. Four signals, each with a source in viability-data.md:
| signal | what it measures | where it comes from |
|---|---|---|
| maintenance | is anyone merging | the stall test: open PRs against merges and commits since 2026-03-01 |
| installed base | who else is exposed to the same risk | npm downloads, and the version spread where the registry exposes it |
| business model | what happens to the free tier if the company needs money | license pages, price pages, the paid package’s download share |
| exit cost | what a page pays to leave | column definitions, custom renderers, the server contract |
Money is read as a durability signal only. A paid tier’s price is a fact a reader needs; a company’s funding is not on these pages.
Three libraries get their own file because their signals point in different directions
at once: Grid.js (the stall against the install base), AG Grid (the Enterprise line
against the MIT core), and DataTables (a major transition and a new commercial tier in
the same quarter). The rest are rows in viability-data.md and paragraphs in
recommendation.md.
DataTables: a major transition and a new commercial tier in one quarter#
What changed#
3.0.0 on 2026-07-24 removed the jQuery dependency and rewrote core and extensions in TypeScript; “DataTables Plus” launched the same day (source: datatables.net/blog/2026/datatables-3, accessed 2026-09-02). Three patch releases followed by 2026-08-31.
Signals#
- Maintenance. 201 commits and 6 merged PRs since March. The project has one author,
and the stall test’s second reading applies: compare inbound to commits, and the
commits are there. Four open issues on the source repository is the smallest backlog in
the survey (source:
observed-data.md, accessed 2026-09-02). - Installed base. 2,873,800 installs a month of the core package. Most of that base is on 2.x with jQuery today; the registry does not expose the 3.x share and this survey does not guess it.
- Business model. Plus is Editor plus CardView at $219 for one developer then $88 a year, $999 for five, $1,699 for ten (source: datatables.net/plus/, accessed 2026-09-02). The blog post says the reason plainly: “The core software is free open-source software, but that alone isn’t a business plan”. Editor has been the paid extension since 2012, so the model is fourteen years old under a new name.
- The bus factor. One author is one author. The mitigation is the contract: the server-side protocol is implemented independently in dozens of backend libraries, and the client is 41 KB of MIT code with no dependencies. The exit is the cheapest in the category.
What a reader is exposed to#
- Two DataTables on the web. For the next year most search results, Stack Overflow answers and tutorials describe 2.x. A team on 3.x has to know which one it is reading.
- Extension compatibility. The release post: “if you use any extensions for DataTables you must update them as well”. A page with Buttons, Responsive or Scroller installed is on a matched set.
Verdict#
The transition removed the one dependency that kept DataTables off framework-free pages and did it without changing the API a server was written against. The commercial tier is old, priced, and confined to editing and card layouts; the list, sort, filter and server-side core stays MIT. A durable choice, with a year of documentation drift ahead.
Grid.js: a stalled repository under a healthy download count#
The numbers#
177,477 installs a month against 0 merged pull requests since March 2026, 0 commits since
March, 32 open pull requests, and a last release on 2024-03-03 (source: observed-data.md,
accessed 2026-09-02). The newest open PR was opened 2026-05-26 (source: api.github.com,
accessed 2026-09-02).
How to read it#
In the four-cell test from ADDING-RESEARCH.md:
| patches merged | none merged | |
|---|---|---|
| patches arriving | healthy | Grid.js is here |
| none arriving | — | finished, or forgotten |
Contributors are still writing code — editable cells, test suites, documentation fixes — and no one with merge rights is taking it. That is different from a finished library, where nothing arrives because nothing is missing. The last push to the repository was in January; the stall is at least seven months old and the release gap is thirty.
Why the downloads do not contradict it#
Downloads lag. A library that stopped being maintained keeps being installed by every
npm ci on every project that already depends on it, and by every tutorial that still
recommends it. 177,477 a month is the size of the exposed base, not of the choosing
base. jsGrid, last released in 2016, records 81,235 a month by the same mechanism
(source: observed-data.md).
What a reader is exposed to#
- No security fixes. No advisory exists against
gridjstoday, and the bundled Preact copy is pinned to whatever 6.2.0 built with. The next Preact advisory has no path into a release. - No fixes at all. 62 open issues will stay open.
- A cheap fork. MIT, 53 KB minified, 32 patches already written and sitting in the queue. A team that depends on it can carry it. That is the only positive reading, and it is a real one.
Verdict#
Not a reason to remove Grid.js from a page that has it and works. A reason not to put it on a new one in September 2026 without knowing that the release the page pins is the last one anyone has committed to.
S4 recommendation#
Three-year reading, by library#
AG Grid Community — will exist and be maintained; the risk is the feature line, not the project. Budget for a major version a year and for the reporting features being Enterprise.
Handsontable — will exist and be maintained; three majors in twelve months and a non-commercial license. A commercial page is a per-developer license from day one, and a non-commercial page should re-read the license each major.
DataTables — will exist; one author, a fourteen-year-old commercial model, the cheapest exit in the category. The first year on 3.x is spent telling 2.x documentation apart from 3.x.
Tabulator — will exist; one maintainer, bursty releases with a fourteen-month gap in 2025, no commercial tier and no dependencies. Types come from DefinitelyTyped and lag the package (6.3.6 types for 6.5.2). The risk is cadence, not direction.
RevoGrid — will exist; 59 releases a year and a Pro tier with no price on the page. The MIT core is a client-side grid and the line to Pro runs through server-side data, which is the feature most likely to be needed next.
SlickGrid — will exist as long as its maintainer does; five open issues, twelve releases a year, a small base. The architecture is the durable part: every virtual grid here descends from it.
regular-table, Cheetah Grid — small, quiet, foundation-backed or corporate-backed, Apache or MIT. Low churn and low activity in the same measure.
Grid.js — stalled. See gridjs-viability.md.
Toast UI Grid — archived, with a vulnerable pin it cannot fix. Migrate.
The durable bets#
Two kinds of durability exist here, and a reader should pick which one they are buying:
- Project durability — AG Grid and Handsontable, on the strength of their commercial engines. The cost is that the engine decides the feature line.
- Contract durability — DataTables’ server protocol and SlickGrid’s DataView-plus-viewport architecture, which outlive any single release because other code is written to them. The cost is that the projects themselves are one person each.
Tabulator sits between: no company, no contract, one maintainer, and the widest MIT feature set on the list. Its durability is the community’s continued interest in a grid that has no paid tier to fund it, which so far has held for eight years.
What to re-check, and when#
- Grid.js: any merge, any release. A single release ends the stall reading.
- Tabulator: the gap between releases. Another year-long silence changes the row.
- AG Grid: the Community/Enterprise page at each major; a feature moving across it.
- DataTables: the 3.x download share once the registry shows it, and Plus pricing.
- Handsontable: the license text, each major.
- Toast UI Grid: nothing; it is archived.
decay_class: fast in metadata.yaml sets the re-check window at ninety days.
Viability data#
All figures from ../S1-rapid/observed-data.md (registry and GitHub, accessed
2026-09-02) unless marked.
Maintenance#
| library | open PRs | merged since 2026-03-01 | commits since 2026-03-01 | last release | cell |
|---|---|---|---|---|---|
| AG Grid Community | 63 | 1,509 | 1,546 | 2026-08-05 | healthy |
| Handsontable | 18 | 1,074 | 1,192 | 2026-09-01 | healthy |
| RevoGrid | 5 | 77 | 363 | 2026-08-24 | healthy |
| SlickGrid | 5 | 79 | 97 | 2026-09-02 | healthy |
| Tabulator | 22 | 41 | 161 | 2026-06-23 | healthy; one maintainer, bursty releases |
| DataTables | 19 | 6 | 201 | 2026-08-31 | author-driven; compare commits, not merges |
| regular-table | 0 | 8 | 24 | 2026-08-11 | healthy, small |
| Cheetah Grid | 0 | 6 | 9 | 2026-08-20 | quiet |
| simple-datatables | 0 | 2 | 14 | 2026-07-30 | quiet |
| Grid.js | 32 | 0 | 0 | 2024-03-03 | stalled |
| Toast UI Grid | 28 | 0 | 0 | 2024-01-10 | archived |
Installed base#
| library | downloads last month | stars | ratio downloads : stars |
|---|---|---|---|
| AG Grid Community | 12,666,018 | 15,578 | 813 |
| DataTables | 2,873,800 | 795 (source repo) | — |
| Handsontable | 1,236,340 | 22,033 | 56 |
| Tabulator | 700,584 | 7,753 | 90 |
| Grid.js | 177,477 | 4,688 | 38 |
| RevoGrid | 133,769 | 3,440 | 39 |
| simple-datatables | 80,225 | 1,608 | 50 |
| regular-table | 74,182 | 403 | 184 |
| SlickGrid | 49,891 | 2,066 | 24 |
| Toast UI Grid | 14,164 | 2,562 | 6 |
| Cheetah Grid | 5,266 | 1,536 | 3 |
Two of these are worth reading twice. AG Grid’s ratio is the highest here because the package is a dependency of many framework wrappers and internal tools; it is installed far more often than it is starred. Handsontable’s is the lowest among the large projects: 22,033 stars for 1.2 million installs is a project many people have looked at and fewer have deployed, which is what a non-commercial license predicts.
Tabulator’s installs split across majors: last week 26% were 6.5.2, 19% 6.3.1, 14% 5.6.1 and 5% 4.9.3 (source: api.npmjs.org/versions/tabulator-tables/last-week, accessed 2026-09-02). A fifth of the base is two majors back.
Business model#
| library | free tier | paid tier | paid tier’s registry share |
|---|---|---|---|
| Tabulator | everything, MIT | none | — |
| Grid.js | everything, MIT | none | — |
| SlickGrid | everything, MIT | none | — |
| regular-table | everything, Apache-2.0 | none | — |
| Cheetah Grid | everything, MIT | none | — |
| DataTables | core and most extensions, MIT | DataTables Plus: Editor, CardView; $219 then $88/yr per developer | not separately published |
| AG Grid | Community, MIT | Enterprise, per developer per deployment, price not on the page | 5,968,698 installs a month, 47% of Community’s |
| RevoGrid | core, MIT | Pro, per developer, price not on the page | not on npm as a public package |
| Handsontable | non-commercial and evaluation only | from $999 per developer | the same package |
| simple-datatables | everything, LGPL-3.0 | none | — |
Sources: license and pricing pages cited on each S1 library page.
Exit cost#
What a page has written against the library, and whether it transfers:
| library | column definitions | custom cell renderers | server contract | events |
|---|---|---|---|---|
| Tabulator | {title, field, formatter} objects | formatter functions on cell | Tabulator’s own parameter names | table.on(...) |
| AG Grid | {field, cellRenderer} objects | renderer classes with init/getGui | infinite/server-side row model callbacks | grid options callbacks |
| Handsontable | {data, type, renderer} | renderer functions on (instance, td, row, col, ...) | none | hooks |
| DataTables | {data, title, render} | render(data, type, row) | the serverSide protocol | dt.on(...) |
| SlickGrid | {id, name, field, formatter} | formatter functions returning HTML | your own getItem | grid.onX.subscribe |
| RevoGrid | {prop, name, cellTemplate} | cellTemplate(h, props) (Stencil h) | none | DOM events |
| Grid.js | array of names or {name, formatter} | Preact h in formatter | server.then | grid.on(...) |
Each is a rewrite of the column layer, which is the layer most application code touches. Nothing transfers except the data array. The one contract that does transfer is DataTables’ server protocol, which other libraries can be adapted to send — a server written for DataTables can stay when the grid changes.