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 withposition: stickyand an empty.scroll-sizersibling 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
estimatedRowHeightpx, so the total scroll size is exact (counttimes the height) and the scrollbar maps precisely to the row index. Opt into variable heights with therowHeightprop (a number, or a(row, index) => pxfunction the virtualizer prefix-sums into exact offsets) — still nogetBoundingClientRect/ 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: noneon 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 composedhy-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.