Skip to content

HyDatagrid

The interactive, feature-rich sibling of hy-table. Where hy-table is a stateless display table, hy-datagrid owns its own data state — sorting, filtering, selection, column sizing / visibility / pinning, and pagination — on a headless @tanstack/table-core engine, and renders every row through a @tanstack/lit-virtual virtualizer so it stays smooth at 10k+ rows.

It looks identical to hy-table at rest: same emphasis / dividers / striped / hoverable axes, same ambient data-density, same --hy-table-*-derived token surface. It only adds interactive chrome on top.

All @tanstack/* imports live behind internal/datagrid-engine.ts; this host never touches the engine directly.

Scroll performance

DOM-based virtualization (not canvas — real table semantics, ARIA, and CSS theming are all preserved), tuned with the techniques AG Grid, RevoGrid, and Google Sheets use. Together they keep scrolling smooth on 10k+ rows AND remove the blank-on-fast-drag flash entirely. In order of importance:

  • Decoupled sticky rendering — the fix for the blank flash on a fast scrollbar-thumb drag. Dragging the thumb is a compositor-thread gesture: Chrome scrolls the layer and paints the destination BEFORE the main-thread scroll event fires (a "partially presented frame"). If rows live in the scrolled coordinate space, the compositor drags them out of view and the destination is blank — unwinnable by rendering harder (AG Grid ships this as open bug #11295). Instead the <table> is pinned with position: sticky and an empty .scroll-sizer sibling owns the scroll height; rows are positioned in VIEWPORT space (translateY(start - scrollOffset)). A sticky layer is composited correctly during a fast scroll, so a missed frame shows the last-rendered rows still pinned in place (a one-frame content lag) instead of white. This is the Google Sheets / "decoupled rendering" technique.
  • Per-frame reposition without a render — on scroll, the already-rendered rows are re-pinned to the live offset with a handful of direct transform writes (no framework re-render). The virtualizer only re-renders — adding / removing rows — when the window's start / end index changes, so ordinary wheel scrolling within a stable window costs ~nothing per frame.
  • Keyed row recycling (the AG Grid / RevoGrid row-cache technique) — rows are keyed by their stable row id, so on a one-row window shift the rows whose id is unchanged keep their DOM AND their cell content — the browser skips re-laying-out those cells (changing every cell's text forces an ~8ms reflow; a transform-only change is ~0.2ms). Only the one or two rows that entered / left are created / destroyed.
  • Stored-size row height — heights come from a size per index, never a DOM measurement. Default: every row is estimatedRowHeight px, so the total scroll size is exact (count times the height) and the scrollbar maps precisely to the row index. Opt into variable heights with the rowHeight prop (a number, or a (row, index) => px function the virtualizer prefix-sums into exact offsets) — still no getBoundingClientRect / ResizeObserver.
  • Cached column geometry — width and pinning are resolved once per column per render (not once per cell), so each window-change render stays cheap.
  • Overscan — a small buffer (8 rows / side, ≈ AG Grid's rowBuffer) covers the window-change render's one-frame latency plus sub-frame wheel scrolling. It is NOT the blank-prevention mechanism (decoupling is), so it stays small to keep each render cheap.
  • overflow-anchor: none on the scroll container prevents scroll-anchoring jumps as the window updates.

For per-row heights, use the rowHeight function (stored-size, math-only); auto-measured (DOM getBoundingClientRect) heights are intentionally not supported, since they reintroduce the per-scroll measurement cost above.

Column virtualization

For very wide grids, set virtualize-columns to additionally window the columns (dual-axis): only the columns in/near the viewport render, via leading/trailing spacer cells, while pinned + selection columns always render. Off by default (narrow grids don't need it; it also disables the fill-to-width behaviour).

Events

  • sort - TableSortDetail — a sortable header was activated (multi-sort emits the primary key/direction).
  • selection-change - HySelectionDetail — the row-selection set committed. Detail: { selectedValues, reason }.
  • column-resize - Detail: DatagridColumnResizeDetail — a resize drag committed.
  • column-visibility-change - Detail: DatagridVisibilityDetail — a visibility-menu toggle.
  • page-change - Detail: DatagridPageDetail — relayed from the composed hy-pagination.
  • filter-change - Detail: DatagridFilterDetail — quick-filter / per-column filter changed.

Slots

  • toolbar-start - Extra leading toolbar content (before the quick filter).
  • toolbar-end - Extra trailing toolbar content (after the column / export controls).
  • cell-{key} - Static override for a column's body cell (mirrors hy-table).
  • header-{key} - Static override for a column's header (mirrors hy-table).

Methods

exportCsv()

Export the current (filtered + sorted) rows to a downloaded CSV file.

clearSelection()

Clear the current row selection.

Built with Lit. Documented with VitePress.