# av-grid

> Framework-agnostic virtualized data grid for the DOM. No dependencies. Built for 100k+ rows.

Latest version **2.11.2** (published 2026-09-14) · MIT license · 0 weekly downloads

## Install

```sh
npm install av-grid
pnpm add av-grid
yarn add av-grid
bun add av-grid
```

## Health

**Score 75/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 2.11.2 |
| Published | 2026-09-14 |
| First published | 2021-02-04 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 0 |
| Unpacked size | 3 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 0 |
| Author | Andriy Viyatyk |
| Maintainers | viyatyk |
| Keywords | grid, datagrid, table, virtualized, virtual-scroll, vanilla-js, typescript |

## Links

- npm: https://www.npmjs.com/package/av-grid
- Repository: https://github.com/andriy-viyatyk/av-grid
- Homepage: https://github.com/andriy-viyatyk/av-grid#readme
- Issues: https://github.com/andriy-viyatyk/av-grid/issues
- npm.io page: https://npm.io/package/av-grid

## Alternatives

- [@lexical/table](https://npm.io/package/@lexical/table.md) — 3.0M weekly downloads
- [mantine-datatable](https://npm.io/package/mantine-datatable.md) — 98.2K weekly downloads
- [react-native-collapsible-tab-view](https://npm.io/package/react-native-collapsible-tab-view.md) — 70.6K weekly downloads
- [@handsontable/vue3](https://npm.io/package/@handsontable/vue3.md) — 16.1K weekly downloads
- [vuewordcloud](https://npm.io/package/vuewordcloud.md) — 7.2K weekly downloads

## Recent versions

- 2.11.2 (latest) — 2026-09-14
- 2.11.1 — 2026-09-11
- 2.11.0 — 2026-09-09
- 2.10.0 — 2026-09-08
- 2.9.2 — 2026-09-07
- 2.9.1 — 2026-09-03
- 2.9.0 — 2026-09-03
- 2.8.0 — 2026-09-01
- 2.7.1 — 2026-08-31
- 2.7.0 — 2026-08-31
- 2.6.1 — 2026-08-31
- 2.6.0 — 2026-08-31
- 2.5.0 — 2026-08-31
- 2.4.0 — 2026-08-31
- 2.3.0 — 2026-08-30
- … 13 more at https://npm.io/package/av-grid/versions

## README

# av-grid

A virtualized data grid that renders straight to the DOM. **No runtime dependencies, no
framework.** Built for **100,000+ rows** with no lag while scrolling, selecting a range, or
editing.

**[▶ Live demo](https://andriy-viyatyk.github.io/av-grid/)** — thirteen examples in the browser,
including a [100,000-row benchmark](https://andriy-viyatyk.github.io/av-grid/examples/09-benchmark.html)
you can run yourself.

```js
import { AVGrid } from "av-grid";

const grid = AVGrid.create("#host", { rows: data });
```

That is the whole minimum call. Columns, header labels, widths, data types, alignment and row keys
are all inferred from the rows.

---

## Why another grid

Most JavaScript grids are comfortable to a few thousand rows and degrade past that — usually
because a hover, a focus move or a growing selection repaints the entire visible window. The cost
of an interaction ends up proportional to the size of the viewport, and sometimes to the size of
the dataset.

av-grid keeps a **cell-level dirty set**. Growing a range selection by one cell repaints exactly
the cells whose state changed — two of them — whether the selection covers 101 rows or 99,001.

The result is a grid whose interaction cost is flat:

| Measured on 100,000 rows × 20 columns, in a real browser | |
|---|---|
| First paint | **~6 ms** |
| Scrolling at the top / at row 99,000 | **60 fps / 60 fps**, a flat-cost ratio of **~1.0×** |
| Full repaint of every visible cell | **0.2 ms, 0 DOM mutations** |
| Dragging a range selection from row 0 to row 99,000 | **2 cells repainted per pointer move**, at both ends |
| Selecting all 100,000 rows | 24.9 ms, **19 rows marked, 0 DOM mutations** |
| Filtering 100k rows down to 40k | 5.9 ms, **1 repaint, 0 DOM mutations** |
| Pasting into 1,000 cells | **1 repaint, 0 DOM mutations** |
| Changing the theme | **0 repaints** |

Every number here was measured by driving the real grid in a real browser, not estimated and not
taken from a synthetic benchmark. The harness is in [`test-boards/`](test-boards/), the full
history — including the measurements that mislead if taken carelessly — is in
[`tasks/benchmark-results.md`](tasks/benchmark-results.md), and you can run it yourself:
[`examples/09-benchmark.html`](examples/09-benchmark.html).

## How it stays flat

Three ideas, and only three:

1. **Visible-window math, done without touching the DOM.** Pure geometry decides which rows and
   columns are on screen, plus an overscan band *in the direction of travel only*.
2. **A cell-level dirty set.** Every state change — focus, selection, hover, an edit, a class hook
   — names the cells it invalidates. A paint re-renders those and reuses everything else *in place*,
   so the element keeps its identity: focus, an open editor and a running transition all survive.
3. **Element pooling.** Cells scrolled out of the window are recycled into cells scrolling in, so a
   scroll frame allocates nothing.

None of it is a framework, and none of it needs one.

## Install

```bash
npm install av-grid
```

Or take the files straight from a CDN — no build step, no bundler:

```
https://cdn.jsdelivr.net/npm/av-grid/dist/av-grid.js
https://cdn.jsdelivr.net/npm/av-grid/dist/av-grid.css
```

Either way you get `dist/av-grid.js` (ESM), `dist/av-grid.umd.cjs` (UMD, global `AVGrid`),
`dist/index.d.ts` (TypeScript types) and `dist/av-grid.css` — about **50 kB gzipped**, everything
included, nothing else pulled in.

Without a bundler, point at the files directly:

```html
<!-- ESM -->
<div id="host" style="height: 400px"></div>
<script type="module">
    import { AVGrid } from "./dist/av-grid.js";
    AVGrid.create("#host", { rows });
</script>

<!-- UMD, no bundler -->
<script src="dist/av-grid.umd.cjs"></script>
<script>
    AVGrid.AVGrid.create("#host", { rows });
</script>
```

The stylesheet is injected on first use and shared by every grid on the page. Pass
`injectStyles: false` and link `dist/av-grid.css` yourself if you would rather control it.

### React

There is an official wrapper, published as a subpath of this same package. `react` and
`react-dom` are optional peer dependencies — a vanilla install pulls in neither.

```tsx
import { AVGrid } from "av-grid/react";

<AVGrid rows={rows} columns={columns} editable sort={sort} onSortChange={setSort} style={{ height: 400 }} />
```

It is a thin lifecycle adapter: options flow down as props, changes come back through the same
callbacks, and `ref.current` is the real grid instance. Inline arrow callbacks cost nothing — they
are proxied, not re-sent — and `sort`, `filters`, `selected` and `searchString` round-trip safely
if you would rather hold them in React state. Full page: [`docs/react-api.md`](docs/react-api.md).

> **Versions 1.x are a different library** — the 2021 React component this project is the
> descendant of, preserved on the
> [`react-1.x`](https://github.com/andriy-viyatyk/av-grid/tree/react-1.x) branch. The rewrite
> shares no API with it and starts at **2.0.0**.

> **The host needs a height.** The grid measures its own root to decide what is on screen, so that
> height has to be definite — `height: 400px`, a flex child, `position: absolute`. If a grid
> renders blank, `grid.getState().viewport.width` is the first thing to read; `0` is the answer to
> the commonest integration failure.

## What it does

- **Virtualized on both axes**, with a sticky header and sticky columns
- **Pinned columns on either edge** (`pinned: "left" | "right"`) and **footer rows pinned to the
  bottom** (`footerRows`) — a grand total formatted by the same columns as the data
- **Column groups** — `group: "Q1"` on the columns that belong together and the header becomes
  two rows, at zero scroll cost
- **Sorting** by header click, with `sortValue` or `rowCompare` for a custom order — and
  **multi-column sort** (`multiSort: true`, Ctrl+click appends a level)
- **Filtering** — a searchable checklist per column, cascaded against the other filters, a built-in
  **text filter** (`filterType: "text"` — contains / equals / starts with), a bar of removable
  chips, `localStorage` persistence, and host-defined filter types
- **Server-side data** — `externalFilter` / `externalSort` keep the whole filter and sort UI while
  the host owns the row set: the grid never re-filters or reorders a page the server already did
- **Cell focus and full keyboard navigation**, Excel-style — with grid ARIA semantics and
  `aria-sort` (see [what conformance we claim](docs/capabilities.md#accessibility--what-conformance-we-claim))
- **Range selection** by drag or by shift, and row selection through a checkbox column
- **In-cell editing** with validation, a virtualized dropdown for columns with `options`, and
  host-supplied editors
- **Clipboard** copy / cut / paste, Excel-compatible, plus Copy as headers / JSON / HTML table
- **A context menu** you can extend, replace, or turn off
- **Rows and columns** added and deleted by button, keyboard or API
- **Column resize and reorder**
- **Themed entirely from CSS custom properties** — no build step, no CSS-in-JS

### Customization

Seven hooks cover what a host actually wants to change, all of them plain functions on a column or
on the options:

| I want to… | Hook |
|---|---|
| draw a cell myself | `Column.render` |
| colour a cell, a header, or a whole row | `Column.cellClass`, `Column.headerClass`, `rowClass` |
| edit with my own control | `Column.editor` |
| filter by something other than a value list | `Column.filter` |
| copy something other than what is on screen | `Column.copyValue` |
| sort by something other than the value | `Column.sortValue` |

No registration calls and no plugin system — a hook is an object property, so reuse is a shared
`const`. Notably, a host `match` in a custom filter is *cheaper* than the built-in test it
replaces (1.7 ms against 2.1 ms on 100,000 rows), and `copyValue` is 6× cheaper than the fallback
it overrides.

## Examples

Twelve standalone files, one topic each, every one meant to be copied whole. The names below open
the **[live demo](https://andriy-viyatyk.github.io/av-grid/)**; the source of each is one file in
[`examples/`](examples/), with no build step of its own.

| | | |
|---|---|---|
| [01-minimal](https://andriy-viyatyk.github.io/av-grid/examples/01-minimal.html) | the smallest thing that works | [source](examples/01-minimal.html) |
| [02-columns](https://andriy-viyatyk.github.io/av-grid/examples/02-columns.html) | widths, types, alignment, inference | [source](examples/02-columns.html) |
| [03-cell-rendering](https://andriy-viyatyk.github.io/av-grid/examples/03-cell-rendering.html) | `render`, `formatValue`, class hooks | [source](examples/03-cell-rendering.html) |
| [04-sorting-filtering](https://andriy-viyatyk.github.io/av-grid/examples/04-sorting-filtering.html) | sorting, search, filters, the filter bar | [source](examples/04-sorting-filtering.html) |
| [05-selection-keyboard](https://andriy-viyatyk.github.io/av-grid/examples/05-selection-keyboard.html) | focus, ranges, row selection, the keyboard map | [source](examples/05-selection-keyboard.html) |
| [06-editing](https://andriy-viyatyk.github.io/av-grid/examples/06-editing.html) | `editable`, validation, dropdowns | [source](examples/06-editing.html) |
| [07-clipboard](https://andriy-viyatyk.github.io/av-grid/examples/07-clipboard.html) | copy, cut, paste, Copy as… | [source](examples/07-clipboard.html) |
| [08-theming](https://andriy-viyatyk.github.io/av-grid/examples/08-theming.html) | the CSS custom-property contract | [source](examples/08-theming.html) |
| [09-benchmark](https://andriy-viyatyk.github.io/av-grid/examples/09-benchmark.html) | the 100k harness — run it yourself | [source](examples/09-benchmark.html) |
| [10-customization](https://andriy-viyatyk.github.io/av-grid/examples/10-customization.html) | all seven hooks in one file | [source](examples/10-customization.html) |
| [11-host-integration](https://andriy-viyatyk.github.io/av-grid/examples/11-host-integration.html) | `rowNoun`, `highlightString`, `extraElement`, menu ids | [source](examples/11-host-integration.html) |
| [12-react](https://andriy-viyatyk.github.io/av-grid/examples/12-react.html) | the React wrapper, no build step | [source](examples/12-react.html) |
| [13-report](https://andriy-viyatyk.github.io/av-grid/examples/13-report.html) | pinned columns and a footer band — a report shape | [source](examples/13-report.html) |
| [14-groups](https://andriy-viyatyk.github.io/av-grid/examples/14-groups.html) | column groups and multi-column sort | [source](examples/14-groups.html) |
| [15-external-data](https://andriy-viyatyk.github.io/av-grid/examples/15-external-data.html) | host-owned filtering and sorting, and the text filter | [source](examples/15-external-data.html) |
| [persephone-board](https://andriy-viyatyk.github.io/av-grid/examples/persephone-board/) | the grid inside a Persephone board | [source](examples/persephone-board/) |

## Documentation

| | |
|---|---|
| [`docs/api.md`](docs/api.md) | **The complete public surface** — every option, column field, method and callback, the filter API, the keyboard map, the CSS tokens, the DOM contract |
| [`docs/react-api.md`](docs/react-api.md) | **The React API** — `av-grid/react`: the component and props, the update lanes, the instance ref, the filter bar, the non-goals |
| [`docs/capabilities.md`](docs/capabilities.md) | What each subsystem does and what it measures |
| [`docs/architecture.md`](docs/architecture.md) | The source tree, file by file |
| [`docs/invariants.md`](docs/invariants.md) | The three rules that carry the performance — read before changing the render path |
| [`docs/releasing.md`](docs/releasing.md) | How a version gets cut and published |

## Provenance

av-grid is a port of the React grid inside
[Persephone](https://github.com/andriy-viyatyk/persephone), where its virtualization engine has
been running against 100k-row datasets. That engine was already framework-free; this project
reimplements the rendering layer in plain DOM so the same performance is available to any page.

**It is not AG Grid**, despite the one-character distance. There is no shared API, no shared code
and no shared history — `AVGrid.create(host, options)` is the whole entry point, and
[`docs/api.md`](docs/api.md) is the whole surface.

The API is deliberately shaped for **an AI agent to use without reading the docs first** — the
minimum call infers everything, the vocabulary is `key` / `name` with no aliases, and validation is
loud and specific rather than silent. Persephone Boards are written by agents, and they were the
first consumer.

## Development

```bash
npm test              # vitest — 737 tests
npm run typecheck     # tsc --noEmit, strict
npm run build         # typecheck + lib build to dist/ (ESM + UMD + types + CSS)
npm run build:board   # bundle src/ into the test boards
```

Tests sit next to their subject as `*.test.ts`. They run under happy-dom, which does **no layout** —
so a green suite proves logic and proves nothing about rendering or performance. That is what the
boards under [`test-boards/`](test-boards/) are for: a real browser, real layout, and a harness on
one global per board. Anything touching the render path gets a benchmark re-run and a row appended
to [`tasks/benchmark-results.md`](tasks/benchmark-results.md).

## License

[MIT](LICENSE)

---
_Source: https://npm.io/package/av-grid · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
