Compute a virtual list's visible range
Rendering ten thousand rows kills a page, so virtualised lists render only the rows in view plus a small buffer, and offset them so the scrollbar still behaves. All of that is one small pure calculation, and it is worth getting exactly right — off by one here and rows flicker at the edges.
Implement visibleRange({ scrollTop, rowHeight, viewportHeight, totalRows, overscan = 0 }) returning { start, end, offsetTop }, where start and end are row indices and end is **inclusive**.
Treat a negative scrollTop as 0, since overscroll bounce can produce one. Then start is Math.floor(scrollTop / rowHeight) - overscan, clamped to at least 0 and at most totalRows - 1. And end is Math.ceil((scrollTop + viewportHeight) / rowHeight) - 1 + overscan, clamped to at most totalRows - 1 and never less than start. offsetTop is start * rowHeight — the padding or translateY that puts the first rendered row in the right place.
When totalRows is 0 there is nothing to render, so return exactly { start: 0, end: -1, offsetTop: 0 }.
Arguments
options{ scrollTop: number; rowHeight: number; viewportHeight: number; totalRows: number; overscan?: number }- The pixel-space inputs from the scroller: current scroll offset, per-row height, visible height, total row count, and an optional buffer of extra rows to render on each side.
Returns
{ start: number; end: number; offsetTop: number } — The inclusive row index range to render plus the pixel offset to apply so the first rendered row sits in the right place.
Examples
visibleRange({ scrollTop: 0, rowHeight: 40, viewportHeight: 400, totalRows: 1000 });
// => { start: 0, end: 9, offsetTop: 0 }At the top of a list of 40px rows, ten rows cover 400px of viewport.
visibleRange({ scrollTop: 100, rowHeight: 40, viewportHeight: 400, totalRows: 1000 });
// => { start: 2, end: 12, offsetTop: 80 }A partial row at each edge means eleven rows render, offset by two.
visibleRange({ scrollTop: 400, rowHeight: 40, viewportHeight: 400, totalRows: 1000, overscan: 3 });
// => { start: 7, end: 22, offsetTop: 280 }Overscan adds three extra rows above and below the strict range.
What it has to do
- Return
{ start, end, offsetTop }with an inclusiveend. - Subtract
overscanfrom the start and add it to the end, defaultingoverscanto 0. - Clamp both indices into
0 .. totalRows - 1and never letendfall belowstart. - Compute
offsetTopasstart * rowHeight. - Return
{ start: 0, end: -1, offsetTop: 0 }whentotalRowsis 0.
Notes
endis **inclusive** so a caller can writefor (let i = start; i <= end; i += 1)without off-by-one worries. The{ start: 0, end: -1 }sentinel for the empty case makes that loop iterate zero times.offsetTopis what you apply to the container of the rendered rows — either aspadding-topor as atranslateY— so the scrollbar still thinks the list is full height.
Stuck? The javascript course covers everything this challenge needs.
- 1.computes the first window at the top of the list
- 2.handles a partially scrolled row
- 3.applies overscan on both sides
- 4.clamps the end at the last row
- 5.returns an empty range for an empty list
These assertions run in your browser the moment you hit Run. Pass all of them to claim the XP.
Discussion
Sign in to postNo comments yet. Be the first to say something.