01 — Trees Are Lists in Disguise
A file tree looks hierarchical, but a virtual scroller needs a flat array. The solution: flatten the tree by walking it depth-first, including only nodes whose parents are expanded. The right panel shows a mock file system tree — the structure the user sees.
The key insight: virtualisation libraries don't need tree support. You flatten the tree yourself and hand the virtualizer a list.
02 — Depth-First Flattening
The flattening algorithm is a depth-first walk with one rule: skip a node's children if it's collapsed.
The output is a plain array. Each item carries its depth for indentation — depth × 16px of left padding gives the visual tree structure. The right panel shows the DFS walk producing a flat array with depth values.
03 — Interactive Expand/Collapse
Click folders on the right to expand and collapse. Watch the flat list grow and shrink in real time. The item count badge updates instantly.
When the user clicks a folder, the expanded set changes, the flat list is recomputed, and the virtualizer renders from the new array. This is a full re-flatten — O(visible nodes) — but it's fast because collapsed subtrees are skipped entirely.
04 — Grids: The Two-Dimensional Problem
A grid with 100 rows and 26 columns has 2,600 cells. A viewport might fit 12 rows and 6 columns — 72 cells. Rendering all 2,600 would defeat the purpose of virtualisation.
The solution: run two independent windowing calculations, one for rows and one for columns. The right panel shows a 10×6 visible grid within a larger 100×26 data space.
05 — The Row Window
The row window is identical to a 1D virtual list. Given scrollTop and rowHeight, compute the first visible row and the last visible row. Only mount rows in this range.
For a viewport of 480px height with 40px rows: floor(scrollTop / 40) gives the first row, ceil((scrollTop + 480) / 40) gives the last. Add overscan and you have the row window.
The right panel highlights the row window — the horizontal slice of rows that are mounted.
06 — The Column Window
The column window works the same way but horizontally. Given scrollLeft and columnWidth, compute the first and last visible columns.
The right panel highlights the column window — the vertical slice of columns that are mounted.
07 — Cell Intersection
The final rendering set is the intersection of the row window and column window. A cell is mounted only if its row index is in the row window AND its column index is in the column window.
The right panel shows both windows overlapping, with the intersection — the cells that actually exist in the DOM — highlighted in the accent color. Everything outside the intersection is not rendered.
This is the core of 2D virtualisation. Two 1D calculations, composed by intersection.
08 — Production Patterns
File explorers, JSON viewers, org charts, and expandable tables all use tree virtualization. Spreadsheets, data grids, and kanban boards use 2D grid virtualization.
Tree: @tanstack/virtual + manual flattening. The library doesn't know your data is a tree — you flatten it and pass a list. Libraries like react-arborist wrap this pattern with expand/collapse state management.
Grid: @tanstack/virtual supports both row and column virtualizers. AG Grid and react-data-grid implement the full 2D intersection pattern with built-in cell rendering, selection, and editing.
Both patterns reduce to the same fundamentals from previous stops — the only difference is how you compute the “visible items” list from a more complex data structure. The Spreadsheet system design problem explores these patterns in a real-world context.