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.

LibraryHow it worksBest for
TabulatorVirtual-window renderer, 37 modules, one MIT file; UMD global TabulatorClient-side reports: grouping, column totals and CSV/XLSX download are all in the core
AG Grid CommunityVirtual rows and columns; client-side and infinite row models; Theming API injects CSSKeyboard and screen-reader work on a free grid; the strongest maintenance signal here
DataTablesEnhances a native <table>; paged by default; the serverSide request contractServer-rendered list pages over a database; the cheapest exit in the category
HandsontableSpreadsheet: range selection, clipboard, fill, undo, validators; virtual rows; 3 themes × light/darkData entry that feels like Excel, for non-commercial use or under a per-developer license
SlickGridDataView + virtual viewport, the original architecture; TypeScript, IIFE and ESM; SortableJS as a globalTeams 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 columnsPages built from custom elements and custom properties
Grid.jsBundled Preact renders every row, or one page with pagination; server is a fetch wrapperA short list on a content page, with the stall priced in
Toast UI GridArchived; requires three unbundled externals including xlsx ^0.17.1Migrating away from
Cheetah GridCanvas: no row DOM at allRaw scroll speed where the accessibility tree does not matter
regular-tableFINOS custom element rendering a virtual native <table> from a data-listener callbackBuilding your own grid on a 10 KB virtual table
simple-datatablesDataTables-shaped vanilla grid; paged by defaultA DataTables-style table where LGPL is acceptable
TanStack Table coreHeadless: computes row models, renders nothing; a vanilla example ships in the repoOwning 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 casePick
server rendered listDataTables — the serverSide contract, 44 KB, enhances a native table; Tabulator’s remote modes second
client side reportTabulator — GroupRows, ColumnCalcs and Download in one MIT file; SlickGrid’s DataView second
accessibility auditAG Grid Community — WCAG 2.0 AA claim, roles, keyboard focus moves; a native-table grid (DataTables) for small data
spreadsheetHandsontable if the license permits; RevoGrid’s MIT core for range editing without formulas
large client sideA virtual grid from the L2 table — SlickGrid, Tabulator, AG Grid, RevoGrid, Handsontable — never full-render or paged
runtime themeRevoGrid or AG Grid — custom properties that cascade from the container; Handsontable’s must be set on its own root
small content listDataTables at 44 KB and maintained; Grid.js at 19 KB with the stall priced in

Avoid

LibraryReasonRisk
Toast UI GridRepository archived 2024; xlsx pinned to a vulnerable range it cannot leaveHIGH
Grid.js for a new productNo merges or commits since March 2026; last release March 2024MEDIUM
Any full-render configuration past 10,000 rowsGrid.js and DataTables with paging off did not finish 100,000 rows in 60 s in S2MEDIUM

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, columnheader and gridcell attributes 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 with customElements.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#

version36.1.0, released 2026-08-05
licenseMIT (Community); ag-grid-enterprise is a commercial EULA
dependenciesag-stack, ag-charts-types
downloads, last month12,666,018 (Community); 5,968,698 (Enterprise)
stars15,578
open issues / open PRs68 / 63
PRs merged / commits since 2026-03-011,509 / 1,546
releases in 12 months14

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 filterset filter (the default filter in Enterprise)
pagination, CSV exportExcel export
client-side and infinite row modelsserver-side and viewport row models
cell editing, cell renderers, column pinning and resizingrow grouping, aggregation, pivot
tree datamaster/detail, range selection, clipboard
keyboard navigation, accessibilitycolumn 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.

librarylast releaseopen PRsmerged since 2026-03-01reading
Grid.js2024-03-03320stalled (core set, see gridjs.md)
canvas-datagrid2023-05-2260stalled; 142 open issues
List.js2021-01-17230stalled; and a list filter, not a grid
jsGrid2016-12-15210finished a decade ago; 81,235 installs a month regardless
Glide Data Grid2024-02-03370stalled; React-only, out of scope on both counts
Toast UI Grid2024-01-10280archived (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 one import, 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:

librarypackageinwhy
Tabulatortabulator-tablescorethe one most often named for this job
Grid.jsgridjscorethe smallest full grid; bundles Preact internally
AG Grid Communityag-grid-communitycorethe most-installed grid on npm with a free tier
Handsontablehandsontablecorethe spreadsheet-shaped grid; non-commercial free tier
DataTablesdatatables.netcorethe oldest, and it dropped jQuery in 3.0 (July 2026)
SlickGridslickgridcorethe original virtual-scrolling grid, maintained as a fork
RevoGrid@revolist/revogridcorea web component; the only one here built as a custom element
Toast UI Gridtui-gridcore, archivedstill installed monthly; the repository is archived
Cheetah Gridcheetah-gridalsocanvas rendering, no row DOM
regular-tableregular-tablealsoa FINOS virtual <table> element
simple-datatablessimple-datatablesalsoa DataTables-shaped vanilla grid under LGPL
TanStack Table core@tanstack/table-corealso, headlessfails 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:

strategywhat exists in the DOM for 100,000 rowslibraries
virtual windowthe rows in view plus a buffer; scrolling swaps themTabulator, AG Grid, Handsontable, SlickGrid, RevoGrid, regular-table
pagedone page of rows; the rest is an arrayDataTables (default), simple-datatables (default), Grid.js with pagination
full renderevery rowGrid.js (default), DataTables with paging: false
canvasno 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 in harness/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 (role and aria-* 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#

  1. Where each strategy’s row-count cliff sits on the same data.
  2. What a page pays in bytes for each, measured on the file it would load.
  3. Whether the accessibility claims survive an automated scan and a keyboard.
  4. 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#

version3.0.3, released 2026-08-31
licenseMIT
dependenciesnone
downloads, last month2,873,800 (datatables.net core)
stars795 on DataTables/DataTablesSrc, the source repository
open issues / open PRs4 / 19
PRs merged / commits since 2026-03-016 / 201
releases in 12 months11

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#

version6.2.0, released 2024-03-03
licenseMIT
dependenciespreact (bundled into the UMD build)
downloads, last month177,477
stars4,688
open issues / open PRs62 / 32
PRs merged / commits since 2026-03-010 / 0
last push2026-01-29
releases in 12 months0

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#

version18.1.0, released 2026-09-01
license“SEE LICENSE IN LICENSE.txt”: non-commercial free, commercial paid
dependenciesnone
downloads, last month1,236,340
stars22,033
open issues / open PRs27 / 18
PRs merged / commits since 2026-03-011,074 / 1,192
npm publishes in 12 months676, 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#

packagelatestreleasedfirst publishedlicense (npm)depsreleases 12 mo / totaldownloads last month
tabulator-tables6.5.22026-06-232018-09-23MIT04 / 105700,584
gridjs6.2.02024-03-032013-09-05MIT10 / 76177,477
ag-grid-community36.1.02026-08-052018-08-21MIT214 / 14312,666,018
handsontable18.1.02026-09-012016-03-04SEE LICENSE IN LICENSE.txt0676 / 17941,236,340
datatables.net3.0.32026-08-312015-10-07MIT011 / 752,873,800
slickgrid5.20.02026-09-022013-02-22MIT112 / 14149,891
tui-grid4.21.222024-01-102017-07-27MIT40 / 9614,164
@revolist/revogrid4.27.02026-08-242020-05-04MIT059 / 646133,769
cheetah-grid2.2.02026-08-202017-11-02MIT01 / 1325,266
regular-table0.9.02026-08-112020-05-23Apache-2.0014 / 5974,182
simple-datatables10.3.02026-07-302018-08-17LGPL-3.024 / 10380,225
@tanstack/table-core9.2.42026-08-282022-03-29MIT197 / 29481,836,911
@finos/perspective-viewer3.8.02025-09-032019-04-30Apache-2.021 / 16256,069
w2ui2.0.02023-04-262015-11-21MIT00 / 54,283
frappe-datatable1.20.72026-07-222018-02-20MIT39 / 105300,208
canvas-datagrid0.4.72023-12-292016-11-30BSD-3-Clause10 / 20827,793
jsgrid1.5.32016-12-152014-12-27MIT00 / 1281,235
list.js2.3.12021-01-172014-08-30MIT10 / 11216,210
@syncfusion/ej2-grids34.2.62026-09-012017-05-10SEE LICENSE IN license1555 / 477749,645
@bryntum/grid7.3.52026-08-212024-03-11MIT013 / 14308
zinggrid2.2.02026-06-112018-10-23None01 / 49624
@glideapps/glide-data-grid6.0.32024-02-032020-11-10MIT32 / 3101,206,594
ag-grid-enterprise36.1.02026-08-052016-03-08Commercial214 / 2435,968,698

Repository and the stall test (PRs merged since 2026-03-01, commits since 2026-03-01)#

packagerepostarsopen issuesopen PRsmergedcommitslast pushlatest releasearchived
tabulator-tablestabulator-tables/tabulator7,75336422411612026-09-016.5.2 2026-06-23no
gridjsgrid-js/gridjs4,6886232002026-01-296.2.0 2024-03-03no
ag-grid-communityag-grid/ag-grid15,57868631,5091,5462026-09-02release-36.1.0 2026-08-05no
handsontablehandsontable/handsontable22,03327181,0741,1922026-09-0218.1.0 2026-09-01no
datatables.netDataTables/DataTablesSrc79541962012026-09-013.0.3 2026-08-31no
slickgrid6pac/SlickGrid2,0665579972026-09-025.20.0 2026-09-02no
tui-gridnhn/tui.grid2,56237628002024-01-10v4.21.22 2024-01-10yes
@revolist/revogridrevolist/revogrid3,440185773632026-09-01v4.27.0 2026-08-24no
cheetah-gridfuture-architect/cheetah-grid1,536360692026-08-20[email protected] 2026-08-20no
regular-tablefinos/regular-table4031908242026-08-19no
simple-datatablesfiduswriter/simple-datatables1,60802142026-07-30no
@tanstack/table-coreTanStack/table28,398162554042026-08-31@tanstack/[email protected] 2026-08-28no
@finos/perspective-viewerperspective-dev/perspective11,1602026-09-01v5.3.0 2026-08-25no
w2uivitmalina/w2ui2,74008642026-08-242.0.0 2023-04-26no
frappe-datatablefrappe/datatable1,332481411282026-07-22v1.20.7 2026-07-22no
canvas-datagridTonyGermaneri/canvas-datagrid1,5741426002025-09-18v0.4.7 2023-05-22no
jsgridtabalinas/jsgrid1,51935521002023-04-04v1.5.3 2016-12-15no
list.jsjavve/list.js11,20817923002025-04-27v2.3.1 2021-01-17no
@syncfusion/ej2-gridssyncfusion/ej2-javascript-ui-controls4491551302026-09-01v34.2.6 2026-09-01no
@bryntum/grid— (no public repository located)
zinggriderror: gh: Not Found (HTTP 404)
@glideapps/glide-data-gridglideapps/glide-data-grid5,3249337002026-01-21v6.0.3 2024-02-03no
ag-grid-enterprise— (no public repository located)

Browser entry fields declared in package.json#

packagemainmodulebrowser / unpkg / jsdelivrtypes
tabulator-tablesdist/js/tabulator.jsdist/js/tabulator_esm.mjs
gridjsdist/gridjs.jsdist/gridjs.module.jsdist/index.d.ts
ag-grid-community./dist/package/main.cjs.js./dist/package/main.esm.mjs./dist/types/src/main.d.ts
handsontableindexindex.mjsdist/handsontable.full.min.js./index.d.ts
datatables.netjs/dataTables.jsjs/dataTables.mjs./types/types.d.ts
slickgrid./dist/cjs/index.js./dist/esm/index.js./dist/types/index.d.ts
tui-griddist/tui-grid.jstypes/index.d.ts
@revolist/revogriddist/index.cjs.jsdist/index.jsdist/revo-grid/revo-grid.esm.jsdist/types/index.d.ts
cheetah-griddist/main.mjsdist/main.umd.js
regular-tabledist/esm/regular-table.jsdist/esm/regular-table.jsdist/esm/regular-table.jsdist/esm/regular-table.d.ts
simple-datatablesdist/index.jsdist/module.jsdist/umd/simple-datatables.jsdist/index.d.ts
@tanstack/table-core./dist/index.d.ts
@finos/perspective-viewerdist/cdn/perspective-viewer.jsdist/esm/perspective-viewer.d.ts
w2uiw2ui-2.0.es6.min.js
frappe-datatabledist/frappe-datatable.cjs.jsdist/frappe-datatable.min.js
canvas-datagrid./dist/canvas-datagrid.js./dist/canvas-datagrid.module.js./dist/types.d.ts
jsgriddist/jsgrid.js
list.jssrc/index
@syncfusion/ej2-grids./dist/ej2-grids.umd.min.js./index.jsindex.d.ts
@bryntum/grid
zinggridindex-commonjs.jsindex.jsZingGridindex.d.ts
@glideapps/glide-data-griddist/cjs/index.jsdist/esm/index.jsdist/esm/index.jsdist/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.1
  • ag-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.4
  • ag-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.4
  • handsontable: GHSA-hf66-r44g-p7j9 CVE-2021-23446 high 2021-09-30 Inefficient Regular Expression Complexity in handsontable | < 10.0.0 fixed:10.0.0
  • datatables.net: GHSA-h73q-5wmj-q8pj CVE-2021-23445 medium 2021-09-29 Cross site scripting in datatables.net | < 1.11.3 fixed:1.11.3
  • datatables.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.22
  • tui-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#

situationfirst lookbecause
list page over a server, rows never all downloadedDataTablesthe request contract, 41 KB, native <table>
client-side report with group-by, totals, CSV or XLSX download, one MIT fileTabulatorGroupRows, ColumnCalcs and Download are in the core
spreadsheet editing, formulas, paste from Excel, non-commercial or licensedHandsontableit is a sheet; the license is the gate
the most capable free grid on keyboard and screen reader, reporting features not neededAG Grid Communitythe vendor’s WCAG work and issue history; 495 KB
the page is built from custom elements and custom propertiesRevoGrid<revo-grid>, 116 variables
a virtual viewport and a DataView, with the team writing the restSlickGridthe original architecture, seven files
own the <table> markup, borrow the row modelTanStack Table coreheadless by design
a short list that fits on one page, minimum bytesGrid.js, with the stall priced in16 KB, full render

Excluded, and why#

  • Toast UI Grid — archived repository, vulnerable xlsx pin.
  • 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#

version4.27.0, released 2026-08-24
licenseMIT (core); RevoGrid Pro is a per-developer license
dependenciesnone
downloads, last month133,769
stars3,440
open issues / open PRs18 / 5
PRs merged / commits since 2026-03-0177 / 363
releases in 12 months59

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#

version5.20.0, released 2026-09-02
licenseMIT
dependenciessortablejs (must be loaded as a global)
downloads, last month49,891
stars2,066
open issues / open PRs5 / 5
PRs merged / commits since 2026-03-0179 / 97
releases in 12 months12

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#

version6.5.2, released 2026-06-23
licenseMIT
dependenciesnone
downloads, last month700,584
stars7,753 (repository moved to the tabulator-tables organization)
open issues / open PRs364 / 22
PRs merged / commits since 2026-03-0141 / 161
releases in 12 months4
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:

  • ajaxURL and ajaxRequestFunc fetch rows; ajaxParams adds query parameters.
  • paginationMode: "remote" sends page and size; sortMode: "remote" and filterMode: "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.

version4.21.22, released 2024-01-10
licenseMIT
dependenciesdompurify, tui-date-picker, tui-pagination, xlsx ^0.17.1
downloads, last month14,164
stars2,562
PRs merged / commits since 2026-03-010 / 0

Source: observed-data.md.

Two things a reader still using it should know:

  • The xlsx dependency is pinned to a vulnerable range. ^0.17.1 cannot 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) against xlsx, 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.js calls require("tui-date-picker"), require("tui-pagination") and require("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:

levelfilerung
L1 delivery cost — the bytes a <script> page loads, from the npm tarballresults-l1-delivery.mdmeasured-local
L2 render at 1k, 10k and 100k rows — first paint, DOM, heap, sort, scrollresults-l2-rows.mdmeasured-local
L3 accessibility — axe-core on the rendered grid, ARIA roles, keyboard focusresults-l3-accessibility.mdmeasured-local
L4 theming — set a documented custom property, look for any element that followsresults-l4-theming.mdmeasured-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#

TabulatorGrid.jsAG Grid CommunityHandsontableDataTablesSlickGridRevoGrid
strategyvirtualfull render, or pagedvirtualvirtualpaged, or full rendervirtualvirtual
virtual scrollingcore, defaultnonecore, defaultcore, defaultScroller extensioncore, defaultcore, default
column virtualizationrenderHorizontalnonecorecorenonenonecore
server-side sort/filter/pageremote modes + ajaxURLserver fetch wrapperinfinite row model (ranges); server-side model is EnterprisenoneserverSide protocolpattern in slick.remotemodel.jsPro
progressive loadprogressiveLoadnoneinfinite row modelnoneScroller extensionpatternPro
group-byGroupRowsnoneEnterprisenone (spreadsheet)RowGroup extensionDataView groupingcore
column totalsColumnCalcsnoneEnterprise (aggregation)formulas via HyperFormulafooter callbackDataView aggregatorsnone
pivotnonenoneEnterprisenonenonenonePro

Editing and export#

TabulatorGrid.jsAG Grid CommunityHandsontableDataTablesSlickGridRevoGrid
cell editingEdit moduleplugincorecore, the whole pointEditor (Plus, paid)editorscore
CSV exportDownloadnonecoreExportFile pluginButtons extensionnonenone
XLSX exportDownload + SheetJSnoneEnterprisenoneButtons + SheetJSnonePro
PDFDownload + jsPDFnonenonenoneButtons + pdfmakenonenone
clipboardClipboardnoneEnterpriseCopyPaste pluginButtonsplugincore
printPrintnoneprint layoutnoneButtonsnonenone

Integration#

TabulatorGrid.jsAG Grid CommunityHandsontableDataTablesSlickGridRevoGrid
browser globalTabulatorgridjsagGridHandsontableDataTableSlickcustom element <revo-grid>
files to load2213271 module + 22 lazy chunks
takes a <div>yesyesyesyesno, a <table>yesthe element is the grid
dependencies0preact (bundled)2 (own packages)00sortablejs (global)0
TypeScript typesDefinitelyTyped, laggingshippedshippedshippedshippedshippedshipped
React/Vue/Angular wrapperscommunityofficialofficialofficialofficialSlickgrid-Universalofficial

Accessibility, theming, license#

TabulatorGrid.jsAG Grid CommunityHandsontableDataTablesSlickGridRevoGrid
vendor a11y claimnone reachablenoneWCAG 2.0 AA, JAWS + VoiceOverWCAG 2.1 AA, NVDA + JAWS + VoiceOverpage 404nonepage 404
roles in sourcegrid, row, gridcell, columnheader, rowgroup, columngroupgrid, status, alertgrid, treegrid, row, gridcelltreegrid, row, gridcell, columnheadernative table + aria-sortgridrich aria-* set
CSS custom properties00195336593116
theme model12 prebuilt stylesheets, SCSS sourceone stylesheet, class overridesTheming API objects, --ag-*3 themes × light/dark, --ht-*separate style packages, dark mode2 themes, SASS5 themes, --revo-grid-*, ThemeDefinition
licenseMITMITMIT; Enterprise EULAnon-commercial free; from $999/devMIT; Plus $219 then $88/yr/devMITMIT; Pro per developer
advisories, ever002 (2024, fixed)1 (2021, fixed)2 (2020–21, fixed)00

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

architecturelibrarieswhat “render 100k rows” means
virtual DOM windowTabulator, AG Grid, Handsontable, SlickGrid, RevoGrid, regular-tablerows in the viewport plus a buffer exist as DOM
pagedDataTables (default), simple-datatables, Grid.js with paginationone page exists as DOM; the rest is an array
full renderGrid.js (default), DataTables with paging: falseevery row exists as DOM
canvasCheetah Gridno 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#

levelsettlescostrungcovers
L0Registry facts — versions, dates, license, downloads, stars, PR stall test, advisorieswhat exists and what state it is indone in S1 (S1-rapid/harvest.py)cited23/23
L1Delivery 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 definedquestion 2, question 4 (static half)~1 hmeasured-local12/12
L2Render 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 Playwrightquestion 1~half a daymeasured-local12/12, with timeouts recorded as results
L3Accessibility scan — axe-core on the rendered grid; ARIA roles present; whether a cell takes keyboard focusquestion 3~2 hmeasured-local12/12
L4Theme probe — set one custom property on the container, read back the computed header backgroundquestion 4 (dynamic half)~1 hmeasured-localthe ones L1 says define variables
L5Remote data — server-side sort/filter/page against a real API, request shape per librarywhich libraries can hand the work to a server, and what the contract is~1 day, needs a server processmeasured-localsubset
L6Screen-reader session — NVDA or VoiceOver reading a gridwhether the axe result means anything to a usera Windows or macOS machine, a personmeasured-localsubset
L7Floor model — the reader runs L2 in their own browserthe reader can check mea real buildmeasured-browser12/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 willread this rowand the finding is
show a list the server pagesDataTables paged: 61 ms at 1k, 238 nodes, 33 ms sortpaging is free below 10k; at 100k the 1,061 ms is the client-side index the server would replace
scroll a reportthe virtual rows’ worst scroll stepTabulator’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-sidethe 100k sort columnSlickGrid 232 ms, Tabulator 527, AG Grid 689, Handsontable 1,200; DataTables paged 199
hold 100k rowsthe heap columnthe 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 auditedthe axe and keyboard tablesAG Grid, then Handsontable; native-table grids for small data
follow a dark-mode toggleL4AG 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 server option 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 its pagination mode 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).

libraryfiles a page loadsJS gzCSS gztotal gzcustom properties loaded / in package
TanStack Table core1 (headless re-export; the size that matters is what the page bundles)1 KB1 KB0 / 0
regular-table210 KB0.8 KB10 KB0 / 4
Grid.js216 KB2.4 KB19 KB0 / 0
simple-datatables227 KB1.1 KB28 KB0 / 0
DataTables + datatables.net-dt241 KB3.8 KB44 KB59 / 59
Cheetah Grid284 KB1.7 KB86 KB6 / 6
Tabulator299 KB3.8 KB102 KB0 / 0
SlickGrid + SortableJS7108 KB5.6 KB114 KB3 / 34 (icons)
Toast UI Grid2, plus three externals it does not bundle101 KB13.0 KB114 KB0 / 0
RevoGrid1 module + 22 lazy chunks (sum of all; a page loads a subset)200 KBin JS200 KB0 / 116
Handsontable (full build + base + ht-theme-main)3439 KB23.1 KB462 KB336 / 336
AG Grid Community1 (Theming API injects CSS)495 KB495 KB0 / 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#

situationwhat eliminates optionsfile
A server-rendered list page — Django, Rails, PHP, Go, or htmx + Alpine — with no bundler, whose rows live in a databaseanything that needs the whole table in the browser; anything without a script-tag builduse-case-server-list.md
A client-side report: group by a column, subtotal, download as CSV or XLSX, from one filegrids whose grouping and export are in a paid tier; grids that page instead of scrolluse-case-client-report.md
A public-sector or regulated page that must pass a WCAG audit with a screen readergrids with no accessibility work; canvas rendering; virtual renderers, conditionallyuse-case-accessible.md
A data-entry sheet: typing into cells, pasting from Excel, formulaseverything that is a grid rather than a spreadsheet; the non-commercial license, conditionallyuse-case-spreadsheet.md
An internal tool holding a hundred thousand rows client-side — a log viewer, a blotter, a queuefull-render and paged strategiesuse-case-large-client.md
A design-system page with a runtime theme and a dark-mode togglestylesheets with no custom propertiesuse-case-themed.md
A short list on a content page: sort, search, forty rows, the fewest byteseverything over about 50 KB; stalled projects, conditionallyuse-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#

situationfirst choicesecondnot this
server-rendered list, rows in a databaseDataTablesTabulator (remote modes)any grid that wants the array
client-side report with group-by, totals, exportTabulatorSlickGrid DataViewAG Grid Community (grouping is Enterprise)
must pass an accessibility auditAG Grid Community; a native <table> grid for small dataDataTables + KeyTableCheetah Grid; any virtual grid without stated ARIA work
a spreadsheetHandsontable, license permittingRevoGridgrids with cell editors
a hundred thousand rows client-sidea virtual grid from the L2 tableCheetah Grid if the tree does not matterfull-render and paged configurations
runtime theme, dark modeRevoGrid, AG GridHandsontable, DataTablesTabulator, Grid.js, SlickGrid
a short list on a content pageDataTablesGrid.js, stall priced inany 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 critical on aria-required-children, which means a grid or row role 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 paintsortworst scroll stepheap
SlickGrid46 ms232 ms79 ms16 MB
Tabulator195 ms527 ms122 ms32 MB
AG Grid Community212 ms689 ms229 ms39 MB
Handsontable1,119 ms1,200 ms155 ms19 MB
regular-table22 ms66 ms (array sort + draw)36 ms13 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.md conflict; 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.md with a virtual grid on the front: Tabulator’s progressiveLoad, AG Grid’s infinite row model, or DataTables with Scroller and serverSide.
  • 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 to use-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-evaluation key 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 :root or [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):

librarycustom propertiesshipped dark mode
Handsontable336yes, every theme
AG Grid Community195 (--ag-*), Theming API parametersyes, via theme parameters
RevoGrid116 (--revo-grid-*)yes, highContrastDark and others
DataTables (-dt package)59 (--dt-*)yes
SlickGrid3 in the Alpine theme, icons onlyno
Tabulator0; 12 prebuilt stylesheets, 163 SCSS variables at buildtabulator_midnight and _site_dark as separate stylesheets
Grid.js0no

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:

signalwhat it measureswhere it comes from
maintenanceis anyone mergingthe stall test: open PRs against merges and commits since 2026-03-01
installed basewho else is exposed to the same risknpm downloads, and the version spread where the registry exposes it
business modelwhat happens to the free tier if the company needs moneylicense pages, price pages, the paid package’s download share
exit costwhat a page pays to leavecolumn 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 mergednone merged
patches arrivinghealthyGrid.js is here
none arrivingfinished, 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 gridjs today, 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#

libraryopen PRsmerged since 2026-03-01commits since 2026-03-01last releasecell
AG Grid Community631,5091,5462026-08-05healthy
Handsontable181,0741,1922026-09-01healthy
RevoGrid5773632026-08-24healthy
SlickGrid579972026-09-02healthy
Tabulator22411612026-06-23healthy; one maintainer, bursty releases
DataTables1962012026-08-31author-driven; compare commits, not merges
regular-table08242026-08-11healthy, small
Cheetah Grid0692026-08-20quiet
simple-datatables02142026-07-30quiet
Grid.js32002024-03-03stalled
Toast UI Grid28002024-01-10archived

Installed base#

librarydownloads last monthstarsratio downloads : stars
AG Grid Community12,666,01815,578813
DataTables2,873,800795 (source repo)
Handsontable1,236,34022,03356
Tabulator700,5847,75390
Grid.js177,4774,68838
RevoGrid133,7693,44039
simple-datatables80,2251,60850
regular-table74,182403184
SlickGrid49,8912,06624
Toast UI Grid14,1642,5626
Cheetah Grid5,2661,5363

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#

libraryfree tierpaid tierpaid tier’s registry share
Tabulatoreverything, MITnone
Grid.jseverything, MITnone
SlickGrideverything, MITnone
regular-tableeverything, Apache-2.0none
Cheetah Grideverything, MITnone
DataTablescore and most extensions, MITDataTables Plus: Editor, CardView; $219 then $88/yr per developernot separately published
AG GridCommunity, MITEnterprise, per developer per deployment, price not on the page5,968,698 installs a month, 47% of Community’s
RevoGridcore, MITPro, per developer, price not on the pagenot on npm as a public package
Handsontablenon-commercial and evaluation onlyfrom $999 per developerthe same package
simple-datatableseverything, LGPL-3.0none

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:

librarycolumn definitionscustom cell renderersserver contractevents
Tabulator{title, field, formatter} objectsformatter functions on cellTabulator’s own parameter namestable.on(...)
AG Grid{field, cellRenderer} objectsrenderer classes with init/getGuiinfinite/server-side row model callbacksgrid options callbacks
Handsontable{data, type, renderer}renderer functions on (instance, td, row, col, ...)nonehooks
DataTables{data, title, render}render(data, type, row)the serverSide protocoldt.on(...)
SlickGrid{id, name, field, formatter}formatter functions returning HTMLyour own getItemgrid.onX.subscribe
RevoGrid{prop, name, cellTemplate}cellTemplate(h, props) (Stencil h)noneDOM events
Grid.jsarray of names or {name, formatter}Preact h in formatterserver.thengrid.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.

Published: 2026-09-02 Updated: 2026-09-02