You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/virtual-grid/README.md

8.8 KiB

VirtualGrid

2D windowing primitive. Virtualises rows and columns — renders only the cells inside the visible rectangle plus row / column overscan. Keeps DOM size constant regardless of rowCount × columnCount. A 1000 × 1000 grid (1M cells) renders ~100 DOM nodes, not a million.

Scope is fixed-size per axis (rowSize + columnSize). Dynamic per-cell sizing in 2D is genuinely ambiguous — a measured cell tells you about its row AND column at the same time, and the delta has to be attributed somehow. Most virtualisers sidestep this by forcing fixed axis sizes. Follow the same convention.

Anatomy

<VirtualGrid.Provider
	rowCount={1000}
	columnCount={50}
	rowSize={32}
	columnSize={120}
>
	{#snippet children({ virtualCells, totalHeight, totalWidth })}
		<VirtualGrid.Viewport style:height="500px" style:width="100%">
			<div style:height="{totalHeight}px" style:width="{totalWidth}px" style:position="relative">
				{#each virtualCells as cell (cell.key)}
					<VirtualGrid.Cell {...cell}>
						{data[cell.rowIndex][cell.columnIndex]}
					</VirtualGrid.Cell>
				{/each}
			</div>
		</VirtualGrid.Viewport>
	{/snippet}
</VirtualGrid.Provider>

Parts

Part Element Description
Provider <div> Root context. Tracks both scroll axes, viewport size, 2D range derivation.
Viewport <div> 2D scroll container (overflow: auto on both axes).
Cell <div> One virtualised cell. Absolutely positioned via translate3d(col, row, 0).

Props

Provider

Prop Type Default Description
rowCount number required Total number of rows.
columnCount number required Total number of columns.
rowSize number 40 Fixed row height (px).
columnSize number 120 Fixed column width (px).
rowOverscan number 2 Extra rows above / below the visible range.
columnOverscan number 2 Extra columns left / right of the visible range.
getRowKey (index: number) => string | number (i) => i Stable key per row.
getColumnKey (index: number) => string | number (i) => i Stable key per column.

Cell

Pass every field of the virtualCell snippet value through to the Cell — rowIndex, columnIndex, rowStart, columnStart, rowSize, columnSize.

Snippet props

{
	virtualCells: Array<{
		rowIndex: number;
		columnIndex: number;
		rowStart: number;
		columnStart: number;
		rowSize: number;
		columnSize: number;
		key: string | number;
	}>;
	totalHeight: number;   // sum of row sizes
	totalWidth: number;    // sum of column sizes
	isEmpty: boolean;
	scrollToCell: (
		rowIndex: number,
		columnIndex: number,
		opts?: {
			rowAlign?: 'start' | 'center' | 'end' | 'auto';
			columnAlign?: 'start' | 'center' | 'end' | 'auto';
			behavior?: ScrollBehavior;
		}
	) => void;
}

Data Attributes

Part Attribute Values
Provider data-virtual-grid Always present
Viewport data-virtual-grid-viewport Always present
Cell data-virtual-grid-cell Always present
Cell data-row-index Row index
Cell data-column-index Column index

ARIA

VirtualGrid is a rendering optimisation; it carries no ARIA roles. For a semantic table / grid, wrap the Provider in the appropriate outer element (e.g. role="grid") and set role="row" / role="gridcell" on Cell yourself.

Sema events

Event Family Verb Target Intent Emitted? When
handle-scroll-row handle scroll viewport — NO Row-axis scroll. Same buzz reasoning as virtual-list — soma does NOT emit.
handle-scroll-column handle scroll viewport — NO Column-axis scroll. Same as above.
shift-navigate-to-cell shift navigate viewport — YES scrollToCell(rowIndex, columnIndex) — programmatic discrete navigation.
commit-set-resize commit set provider neutral YES rowCount or columnCount changed (real dimension change, not initial).

See virtual-list README for the rationale on skipping handle-scroll* emission. No per-component pack — falls back to family bases.

Comparison

Feature Soma Ark UI TanStack React Aria
Dedicated 2D component ✅ ❌ ✅ ⚠️
Fixed row / column sizes ✅ — ✅ ✅
Dynamic row heights ❌¹ — ✅ ⚠️
Dynamic column widths ❌¹ — ✅ ⚠️
Row + column overscan ✅ — ✅ ✅
scrollToCell(row, col, align) ✅ — ✅ ✅
Independent getRowKey / getColumnKey ✅ — ✅ ✅
ARIA-agnostic ✅ — ✅ ❌
Zero external dependencies ✅ — ✅ ❌

¹ Fixed-size only. See notes at the top — dynamic 2D is ambiguous enough that most virtualisers don't offer it. TanStack does, at the cost of complex axis-delta attribution. Not planned.

Usage

100 × 100 spreadsheet

<script lang="ts">
	import { VirtualGrid } from '$soma/components';

	const rows = 100;
	const cols = 100;
	const cell = (r: number, c: number) => `R${r}·C${c}`;
</script>

<VirtualGrid.Provider
	rowCount={rows}
	columnCount={cols}
	rowSize={32}
	columnSize={100}
>
	{#snippet children({ virtualCells, totalHeight, totalWidth })}
		<VirtualGrid.Viewport style:height="400px" style:width="600px">
			<div style:height="{totalHeight}px" style:width="{totalWidth}px" style:position="relative">
				{#each virtualCells as c (c.key)}
					<VirtualGrid.Cell {...c}>
						{cell(c.rowIndex, c.columnIndex)}
					</VirtualGrid.Cell>
				{/each}
			</div>
		</VirtualGrid.Viewport>
	{/snippet}
</VirtualGrid.Provider>

Thumbnail grid with scrollToCell

<button onclick={() => api?.(500, 3, { rowAlign: 'center' })}>
	Jump to image 500-3
</button>

<VirtualGrid.Provider rowCount={1000} columnCount={5} rowSize={180} columnSize={200}>
	{#snippet children({ virtualCells, totalHeight, totalWidth, scrollToCell })}
		{(() => { api = scrollToCell; return ''; })()}
		<VirtualGrid.Viewport style:height="600px">
			<div style:height="{totalHeight}px" style:width="{totalWidth}px" style:position="relative">
				{#each virtualCells as c (c.key)}
					<VirtualGrid.Cell {...c}>
						<img src={images[c.rowIndex * 5 + c.columnIndex]} alt="" />
					</VirtualGrid.Cell>
				{/each}
			</div>
		</VirtualGrid.Viewport>
	{/snippet}
</VirtualGrid.Provider>

Implementation notes

  • Total size is rowCount × rowSize × columnCount × columnSize — the inner canvas sets its height/width to these values so the Viewport's native scrollbar reflects the real scrollable area.
  • Visible range is computed per axis independently (two 1D problems). The cartesian product of visible rows × visible columns gives the cells to render.
  • No ResizeObserver on Cell — fixed sizes mean we never need to measure. That's the main reason VirtualGrid skips dynamic — measuring would change the axes non-orthogonally.
  • One Viewport only — unlike VirtualList, there's no window-scroll variant for VirtualGrid. 2D window-scroll is niche and the scroll semantics are confusing.

Powered by TurnKey Linux.