An open-source implementation of Twitter's VirtualScroller
component: a component for efficiently rendering large lists of variable height items. Automatically measures items as they're rendered and supports items of variable height. Also includes a React component for those who're using React.
DOM (no frameworks):
React:
Rendering really long lists in HTML can be performance intensive which sometimes leads to slow page load times and wasting mobile users' battery. For example, consider a chat app rendering a list of a thousand of the most recent messages: when using React the full render cycle can take up to 100 milliseconds or more on a modern PC. If the chat message component is complex enough (rich text formatting, pictures, videos, attachments, buttons) then it could take up to a second or more (on a modern PC). Now imagine users viewing the website on their aged low-tier smartphones and it quickly results in annoying user experience resulting in them closing the website and the website losing its user base.
In 2017 Twitter completely redesigned their website with responsiveness and performance in mind using the latest performance-boosting techniques available at that time. They wrote an article about it where they briefly mentioned this:
On slower devices, we noticed that it could take a long time for our main navigation bar to appear to respond to taps, often leading us to tap multiple times, thinking that perhaps the first tap didn’t register. It turns out that mounting and unmounting large trees of components (like timelines of Tweets) is very expensive in React. Over time, we developed a new infinite scrolling component called VirtualScroller. With this new component, we know exactly what slice of Tweets are being rendered into a timeline at any given time, avoiding the need to make expensive calculations as to where we are visually.
However, Twitter didn't share the code for their VirtualScroller
component (unlike Facebook, Twitter doesn't share much of their code). This library is an attempt to create an open-source implementation of such VirtualScroller
component for anyone to use in their projects.
There's also an "RFC" for a native VirtualScroller
component where they try to formulate what is a VirtualScroller
component and how it should behave.
It works by measuring each list item's height as it's being rendered and then as the user scrolls it hides the items which are no longer visible and shows the now-visible items as they're scrolled to. The hidden items on top are compensated by setting padding-top
on the container, and the hidden items on the bottom are compensated by setting padding-bottom
on the container. The component listens to scroll
/ resize
events and re-renders the currently visible items as the user scrolls (or if the browser window is resized).
Go to the demo page, open Developer Tools ("Elements" tab), find <div id="root"/>
element, expand it, see <div id="messages"/>
element, expand it and observe the changes to it while scrolling the page.
List items can also have inter-item spacing via margin-top
/ margin-bottom
or border-top
/ border-bottom
, see the Gotchas section for more details on how to do that properly.
npm install virtual-scroller --save
If you're not using a bundler then use a standalone version from a CDN.
The default export is the low-level VirtualScroller
class: it implements the core logic of a VirtualScroller
component and can be used for building a VirtualScroller
component for any UI framework. Hence, it's not meant to be used in applications directly: instead, see the high-level components exported from virtual-scroller/dom
and virtual-scroller/react
packages.
import VirtualScroller from 'virtual-scroller'
const virtualScroller = new VirtualScroller(getContainerElement, items, options)
// Start listening to scroll events.
virtualScroller.listen()
// Stop listening to scroll events.
virtualScroller.stop()
getContainerElement()
function returns a "container" DOM Element where the list will be rendered.items
is the list of items.options
is the list of options.
VirtualScroller
works by updating its state
as the user scrolls the page. The state
provides the calculations on which items should be rendered depending on the scroll position. VirtualScroller
itself doesn't perform any rendering: a developer must either supply getState
/setState
options or onStateChange
option (or both), and those functions are gonna be the ones actually rendering the list based on the VirtualScroller
state
.
The main state
properties are:
items: any[]
— The list of items (can be updated via.setItems()
).firstShownItemIndex: number
— The index of the first item that should be rendered.lastShownItemIndex: number
— The index of the last item that should be rendered.beforeItemsHeight: number
— Thepadding-top
which should be applied to the "container" element: it emulates all items beforefirstShownItemIndex
as if they were rendered.afterItemsHeight: number
— Thepadding-bottom
which should be applied to the "container" element: it emulates all items afterlastShownItemIndex
as if they were rendered.
The following state
properties are only used for saving and restoring VirtualScroller
state
, and normally shouldn't be accessed:
itemStates: object?[]
— A list of item states.itemHeights: number?[]
— A list of measured item heights. If an item's height hasn't been measured yet then it's height isundefined
.itemSpacing: number?
— Inter-item spacing. Isundefined
until at least two items have been rendered.scrollY: number?
— The current page scroll position (page vertical scroll offset). If initialstate
is passed toVirtualScroller
, then the page will be scrolled tostate.scrollY
on.render()
.
Here's an example of implementing virtual-scroller/dom
on top of the low-level VirtualScroller
class (advanced):
import VirtualScroller from 'virtual-scroller'
const items = [
{ title: 'Apple' },
{ title: 'Banana' },
{ title: 'Cranberry' }
]
function renderItem(item) {
const div = document.createElement('div')
div.innerText = item.title
return div
}
const container = document.getElementById('list')
function onStateChange(newState, prevState) {
const {
items,
beforeItemsHeight,
afterItemsHeight,
firstShownItemIndex,
lastShownItemIndex
} = newState
// Set `paddingTop` and `paddingBottom` on the container element:
// it emulates the non-visible items as if they were rendered.
container.style.paddingTop = Math.round(beforeItemsHeight) + 'px'
container.style.paddingBottom = Math.round(afterItemsHeight) + 'px'
// Perform an intelligent "diff" re-render as the user scrolls the page.
// This also requires that the list of `items` hasn't been changed.
// On initial render, `prevState` is `undefined`.
if (prevState && items === prevState.items) {
// Remove no longer visible items.
let i = prevState.lastShownItemIndex
while (i >= prevState.firstShownItemIndex) {
if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
// The item is still visible.
} else {
// The item is no longer visible. Remove it.
container.removeChild(container.childNodes[i - prevState.firstShownItemIndex])
}
i--
}
// Add newly visible items.
let prependBefore = container.firstChild
let i = firstShownItemIndex
while (i <= lastShownItemIndex) {
if (i >= prevState.firstShownItemIndex && i <= prevState.lastShownItemIndex) {
// The item is already being rendered.
// Next items will be appended rather than prepended.
prependBefore = undefined
} else {
if (prependBefore) {
container.insertBefore(renderItem(items[i]), prependBefore)
} else {
container.appendChild(renderItem(items[i]))
}
}
i++
}
} else {
// Re-render the list from scratch.
while (container.firstChild) {
container.removeChild(container.firstChild)
}
let i = firstShownItemIndex
while (i <= lastShownItemIndex) {
container.appendChild(renderItem(items[i]))
i++
}
}
}
const options = { onStateChange }
const virtualScroller = new VirtualScroller(() => element, items, options)
// Start VirtualScroller listening for scroll events.
virtualScroller.listen()
// Stop VirtualScroller listening for scroll events
// when the user navigates to another page:
// router.onPageUnload(virtualScroller.stop)
Available VirtualScroller
options
:
estimatedItemHeight: number
— Is used for the initial render of the list: determines how many list items are rendered initially to cover the screen height plus some extra vertical margin for future scrolling. If not set then the list first renders just the first item, measures it, and then assumes it to be theestimatedItemHeight
from which it calculates how many items to show on the second render pass to fill the screen height plus some extra vertical margin for future scrolling. Therefore, this setting is only for the initial render minor optimization and is not required.
-
state: object
— The initial state forVirtualScroller
. Can be used, for example, to quicky restore the list when it's re-rendered on "Back" navigation. -
customState: object
— (advanced) The initial "custom" state forVirtualScroller
. It can be used to initialize the "custom" part ofVirtualScroller
state in cases whenVirtualScroller
state is used to store some "custom" list state. -
getState(): object
— (advanced) By default,VirtualScroller
managesstate
internally by storing it in an instance variable. For more control, thestate
could be managed externally, in which case a developer should supplygetState
/setState
options, in which casegetState
should return the externally storedVirtualScroller
state
. ReactVirtualScroller
component implementation uses this option. -
setState(newState: object, { willUpdateState, didUpdateState })
— (advanced) By default,VirtualScroller
managesstate
internally by storing it in an instance variable. For more control, thestate
could be managed externally, in which case a developer should supplygetState
/setState
options, in which casesetState
should update the externally storedVirtualScroller
state
(including setting the initialstate
).setState
must also call two functions:willUpdateState(newState, prevState)
right before the state is updated anddidUpdateState(prevState)
right after the state is updated. The list should be re-rendered as part of eithersetState
oronStateChange
. ReactVirtualScroller
component implementation uses this option. -
onStateChange(newState: object, prevState: object?)
— Is called wheneverVirtualScroller
state
is updated (including setting the initialstate
). A developer must either supplygetState
/setState
options oronStateChange
option (or both). The list should be re-rendered as part of eithersetState
oronStateChange
. -
scrollableContainer: Element
— (advanced) If the list is being rendered in a "scrollable container" (for example, if one of the parent elements of the list is styled withmax-height
andoverflow: auto
), then passing the "scrollable container" DOM Element is required for correct operation. This feature is considered experimental. The width and height of thescrollableContainer
shouldn't change unless window is resized. -
bypass: boolean
— Passtrue
to turn offVirtualScroller
behavior and just render the full list of items. -
onItemInitialRender(item)
— Is called for each item the first time it's rendered. Is guaranteed to be called at least one time for each item rendered, though could also be called multiple times. For an example of using such feature, consider a list of items that must be somehow preprocessed (parsed, enhanced, etc) before being rendered, and such preprocessing puts some load on the CPU (and therefore takes some time). In such case, instead of preprocessing the whole list of items upfront, a developer could preprocess the items as they're being rendered thereby eliminating any associated lag/freezing and draining less battery. -
preserveScrollPositionOfTheBottomOfTheListOnMount: boolean
— (advanced) Set totrue
to preserve scroll position relative to the bottom of the list when it's first mounted. A possible use case: consider a forum thread only showing unread posts by default. If a user navigates to such thread, it could show "No new posts" message with a "Show previous" button above it. When the user clicks the "Show previous" button, a<VirtualScroller/>
is mounted with a list of posts. IfpreserveScrollPositionOfTheBottomOfTheListOnMount: true
is set, then, after the list of posts is shown, page scroll will be restored so that the bottom of the list remains on screen so that the user could start scrolling up to read the "previous" posts starting from the most recent ones to the oldest ones. -
shouldUpdateLayoutOnWindowResize(event: Event): boolean
— By default,VirtualScroller
always performs a re-layout on windowresize
event. Theresize
event is not only triggered when a user resizes the window itself: it's also triggered when the user switches into (and out of) fullscreen mode. By default,VirtualScroller
performs a re-layout on all windowresize
events, except for ones that don't result in actual window width or height change, and except for cases when, for example, a video somewhere in a list is maximized into fullscreen. There still can be other "custom" cases: for example, when an application uses a custom "slideshow" component (rendered outside of the list DOM element) that goes into fullscreen when a user clicks a picture or a video in the list. For such "custom" casesshouldUpdateLayoutOnWindowResize(event)
option / property can be specified. -
measureItemsBatchSize: number
— (advanced) (experimental) Imagine a situation when a user doesn't gradually scroll through a huge list but instead hits an End key to scroll right to the end of such huge list: this will result in the whole list rendering at once (because an item needs to know the height of all previous items in order to render at correct scroll position) which could be CPU-intensive in some cases (for example, when using React due to its slow performance when initially rendering components on a page). To prevent freezing the UI in the process, ameasureItemsBatchSize
could be configured, that would limit the maximum count of items that're being rendered in a single pass for measuring their height: ifmeasureItemsBatchSize
is configured, then such items will be rendered and measured in batches. By default it's set to100
. This is an experimental feature and could be removed in future non-major versions of this library. For example, the future React 17 will come with Fiber rendering engine that is said to resolve such freezing issues internally. In that case, introducing this option may be reconsidered.
VirtualScroller
class instance provides methods:
-
listen()
— StartsVirtualScroller
listening for scroll events. Should be called immediately after the list has been rendered on a page. -
stop()
— StopsVirtualScroller
listening for scroll events. Should be called when the list is about to be removed from the page. Once stopped, aVirtualScroller
can't be restarted. -
getState(): object
— ReturnsVirtualScroller
state. Is used for ReactVirtualScroller
component implementation.
-
onItemHeightChange(i: number)
— Should be called whenever a list item's height changes: triggers a re-layout ofVirtualScroller
. This allowsVirtualScroller
to re-measure the new item's height and re-render correctly. CallingonItemHeightChange()
manually could be replaced with detecting item height changes automatically via Resize Observer. For example, when a user clicks an "Expand"/"Collapse" button in a post. CallingonItemHeightChange()
is only required when an item shrinks in height. For example, consider a post with an "Expand"/"Collapse" button: when such post is expanded the next posts in the feed might not be visible yet but if a user clicks the "Collapse" button the post is collapsed and the next posts become visible but they're not yet rendered becauseVirtualScroller
didn't render them previously due to them being invisible. CallingonItemHeightChange(i)
in such case would makeVirtualScroller
re-measure the collapsed post height and perform a re-layout. -
onItemStateChange(i: number, itemState: object)
— Can be used to update a list item's state insideVirtualScroller
state. For example, storing list item's state insideVirtualScroller
state is used in ReactVirtualScroller
component to preserve the state of list items that are unmounted due to being no longer visible: when they're visible again they're re-mounted and their state isn't lost. CallingonItemStateChange()
doesn't trigger a re-layout ofVirtualScroller
because changing a list item's state doesn't necessarily mean a change of its height, so a re-layout isn't necessarily required. If a re-layout is required then callonItemHeightChange(i)
manually. For example, consider a social network feed, each post optionally having an attachment. Suppose there's a post in the feed having a YouTube video attachment. The attachment is initially shown as a thumbnail that expands into an embedded YouTube player on click. If a user expands the video, then scrolls down so that the post with the video is unmounted, then scrolls back up so that the post with the video is re-mounted again, then the video should stay expanded (or maybe not, but you get the idea). -
setItems(newItems: any[], options: object?)
— UpdatesVirtualScroller
items
. For example, it can be used to prepend or append new items to the list. See Dynamically Loaded Lists section for more details. Available options:preserveScrollPositionOnPrependItems: boolean
— Set totrue
to enable "restore scroll position after prepending new items" feature (should be used when implementing a "Show previous items" button). -
getItemCoordinates(i: number): object
— Returns coordinates of item with indexi
relative to the document:top
is the top offset of the item relative to the start of the document,bottom
is the top offset of the item's bottom edge relative to the start of the document,height
is the item's height. -
updateLayout()
— (advanced) Triggers a re-layout ofVirtualScroller
. It's what's called every time on page scroll or window resize. You most likely won't ever need to call this method manually. Still, it can be called manually when the list's top position changes not as a result of scrolling the page or resizing the window. For example, if some DOM elements above the list are removed (like a closeable "info" panel) or collapsed (like an "accordion" panel) then the list's top position changes which means that now some of the previoulsy shown items might go off screen and the user might be seeing a blank area where items haven't been rendered yet because they were off-screen during the previousVirtualScroller
layout.VirtualScroller
automatically performs a layout only on page scroll or window resize; in all other cases, when layout needs to be re-run then call it manually via this instance method.
This is an example of using virtual-scroller/dom
component. It's the source code of the DOM demo.
import VirtualScroller from 'virtual-scroller/dom'
const messages = [
{
username: ...,
date: ...,
text: ...
},
...
]
function renderMessage(message) {
// Message element.
const root = document.createElement('article')
// Message author.
const author = document.createElement('a')
author.setAttribute('href', `/users/${message.username}`)
author.textContent = `@${message.username}`
root.appendChild(author)
// Message date.
const time = document.createElement('time')
time.setAttribute('datetime', message.date.toISOString())
time.textContent = message.date.toString()
root.appendChild(time)
// Message text.
const text = document.createElement('p')
text.textContent = message.text
root.appendChild(text)
// Return message element.
return root
}
const virtualScroller = new VirtualScroller(
document.getElementById('messages'),
messages,
renderMessage
)
// For "Single Page Apps":
// router.onPageUnload(virtualScroller.stop)
DOM VirtualScroller
constructor takes arguments:
container
— Items list container DOMElement
.items
— The items list.renderItem(item)
— Renders anitem
as a DOMElement
.options
—VirtualScroller
options.
Additional options
:
onItemUnmount(itemElement)
— Is called after aVirtualScroller
item DOMElement
is unmounted. Can be used to add DOMElement
"pooling".
DOM VirtualScroller
instance provides methods:
setItems(items, options)
— A proxy for the correspondingVirtualScroller
method.onItemHeightChange(i)
— A proxy for the correspondingVirtualScroller
method.onItemStateChange(i, itemState)
— A proxy for the correspondingVirtualScroller
method.getItemCoordinates(i)
— A proxy for the correspondingVirtualScroller
method.stop()
— A proxy for the correspondingVirtualScroller
method.
This is an example of using the React virtual-scroller/react
component. It's the source code of the React demo.
import React from 'react'
import PropTypes from 'prop-types'
import VirtualScroller from 'virtual-scroller/react'
function Messages({ messages }) {
return (
<VirtualScroller
items={messages}
itemComponent={Message}
)
}
const message = PropTypes.shape({
username: PropTypes.string.isRequired,
date: PropTypes.instanceOf(Date).isRequired,
text: PropTypes.string.isRequired
})
Messages.propTypes = {
messages: PropTypes.arrayOf(message).isRequired
}
function Message({ children: message }) {
const {
username,
date,
text
} = message
return (
<article>
<a href={`/users/${username}`}>
@{username}
</a>
<time dateTime={date.toISOString()}>
{date.toString()}
</time>
<p>
{text}
</p>
</article>
)
}
Message.propTypes = {
children: message.isRequired
}
React <VirtualScroller/>
component receives properties:
items
— The items list.itemComponent
— List item component. Receives the list item as thechildren
property. Can optionally receivestate
andonSaveState()
properties for saving list item state before unmounting it and then restoring that state after the item is re-mounted (for example, this supports "Show more" buttons, "Expand YouTube video" buttons, etc). For best performance, make sure it's aReact.PureComponent
or aReact.memo()
, otherwise it'll be re-rendering as the user scrolls.itemComponentProps: object
— (optional) The props passed toitemComponent
.as
— A component used as a container for the list items. Is"div"
by default.estimatedItemHeight: number
— (optional) TheestimatedItemHeight
option ofVirtualScroller
class.bypass: boolean
— (optional) Thebypass
option ofVirtualScroller
class.preserveScrollPositionOnPrependItems: boolean
— (optional) ThepreserveScrollPositionOnPrependItems
option ofVirtualScroller.setItems()
method.preserveScrollPositionOfTheBottomOfTheListOnMount: boolean
— (optional) ThepreserveScrollPositionOfTheBottomOfTheListOnMount
option ofVirtualScroller
.measureItemsBatchSize: number
— (optional) ThemeasureItemsBatchSize
option ofVirtualScroller
.
onItemInitialRender(item)
— (optional) TheonItemInitialRender
option ofVirtualScroller
class.
initialState: object
— (optional) The initial state forVirtualScroller
: thestate
option ofVirtualScroller
. For example, can be used to quicky restore the list on "Back" navigation.initialCustomState: object
— (advanced) (optional) The initial "custom" state forVirtualScroller
: thecustomState
option ofVirtualScroller
. It can be used to initialize the "custom" part ofVirtualScroller
state in cases whenVirtualScroller
state is used to store some "custom" list state.onStateChange(newState: object, prevState: object)
— (optional) Is called wheneverVirtualScroller
state
is updated (including setting the initialstate
). For example, can be used to keepVirtualScroller
state
copy in an instance variable and later incomponentWillUnmount()
persist it somewhere in global application state for quickly restoring it later on "Back" navigation:
import {
getVirtualScrollerState,
setVirtualScrollerState
} from './globalState'
class Example extends React.Component {
componentWillUnmount() {
saveVirtualScrollerState(this.virtualScrollerState)
}
render() {
return (
<VirtualScroller
items={...}
itemComponent={...}
state={hasUserNavigatedBack ? getVirtualScrollerState() : undefined}
onStateChange={state => this.virtualScrollerState = state}/>
)
}
}
itemComponent
receives properties:
children
— The item.state
— Item's state. Can be used for storing and then restoring the state of components which are about to be shown again after being previously hidden. For example, consider a post with an "Expand YouTube Video" button. If a user clicks the button the post is expanded and its height changes. Then the user scrolls down until the post is no longer visible and so it's no longer rendered. Then the user scrolls back up and the post is visible again and so it's rendered again. If the "Expand YouTube Video" state wasn't preserved then the list items would "jump" for the expanded minus non-expanded height difference. To avoid that, some{ videoExpanded: true }
state
could be passed to the post component which would make it auto-expand the video on mount and so the list items wouldn't "jump". For that,onStateChange({ videoExpanded: true })
would have to be called when the user clicks the "Expand YouTube Video" button.onStateChange(itemState)
— A proxy forVirtualScroller.onItemStateChange(i, itemState)
. Call this function to update item'sstate
.onHeightChange()
— A proxy forVirtualScroller.onItemHeightChange(i)
. Call this function to re-measure the item if it shrinks in height.
React <VirtualScroller/>
component instance provides methods:
renderItem(i)
— Calls.forceUpdate()
on theitemComponent
instance for item with indexi
. Does nothing if the item isn't rendered. Is only supported forcomponent
s that areReact.Component
s.getItemCoordinates(i)
— A proxy for the correspondingVirtualScroller
method.updateLayout()
— A proxy for the correspondingVirtualScroller
method.
All previous examples described cases with a static items
list. When there's a need to update the items
list, one could use virtualScroller.setItems(newItems)
instance method. For example, when the user clicks "Show previous items" button, the newItems
should be set to previousItems.concat(currentlyShownItems)
, and when the user clicks "Show next items" button, the newItems
should be set to currentlyShownItems.concat(nextItems)
.
When using virtual-scroller/dom
component, a developer should call .setItems(newItems)
instance method in order to update the items.
When using virtual-scroller/react
React component, it calls .setItems(newItems)
method automatically when new items
property is passed.
The basic equality check (===
) is used to intelligently compare newItems
to the existing items
. If a simple append and/or prepend operation is detected, then the list seamlessly transitions from the current state to the new state, preserving its state and scroll position. If, however, the items have been updated in such a way that it's not a simple append and/or prepend operation, then the entire list is rerendered from scratch, losing its state and resetting the scroll position. There're valid use cases for both situations.
For example, suppose a user navigates to a page where a list of items: object[]
is shown using a VirtualScroller
. When a user scrolls down to the last item in the list, a developer might want to query the database for the newly added items, and then show those new items to the user. In that case, the developer could send a query to the API with afterId: number
parameter being the id: number
of the last item in the list, and the API would then return a list of the newItems: object[]
whose id: number
is greater than the afterId: number
parameter. Then, the developer would append the newItems: object[]
to the items: object[]
, and then call VirtualScroller.setItems()
with the updated items: object[]
, resulting in a "seamless" update of the list, preserving its state and scroll position.
Another example. Suppose a user navigates to a page where they can filter a huge list by a query entered in a search bar. In that case, when the user edits the query in the search bar, VirtualScroller.setItems()
method is called with a list of filtered items, and the entire list is rerendered from scratch. In this case, it's ok to reset the VirtualScroller
state and the scroll position.
When new items are appended to the list, the page scroll position remains unchanged. Same's for prepending new items to the list: the scroll position of the page stays the same, resulting in the list "jumping" down when new items get prepended. To fix that, pass preserveScrollPositionOnPrependItems: true
option to the VirtualScroller
. When using virtual-scroller/dom
component, pass that option when creating a new instance, and when using virtual-scroller/react
React component, pass preserveScrollPositionOnPrependItems
property.
For implementing "infinite scroll" lists, a developer could also use on-scroll-to
component.
VirtualScroller
measures item heights as soon as they've rendered and later uses those measurements to determine which items should be rendered when the user scrolls. This means that things like <img/>
s require special handling to prevent them from changing their size. For example, when rendering a simple <img src="..."/>
first it renders an element with zero width and height and only after the image file header has been parsed does it resize itself to the actual image's width and height. When used inside VirtualScroller
items such images would result in scroll position "jumping" as the user scrolls. To avoid that, any <img/>
s rendered inside VirtualScroller
items must either have their width
and height
set explicitly or have their aspect ratio set explicitly by making them position: absolute
and wrapping them in a parent <div/>
having position: relative
and padding-bottom: ${100/aspectRatio}%
.
If any vertical margin
is set on the list items then this may lead to the list items jumping by the value of that margin when scrolling. The reason is that when the first list item is rendered then there's no padding-top
on the containing <div/>
so the first item's margin "collapses" with outer margins, but when the first item is no longer rendered the containing <div/>
has padding-top
to compensate for that which prevents list item margin from "collapsing" with outer margins. To fix that, don't set any margin-top
on the first item of the list and don't set any margin-bottom
on the last item of the list. An example of fixing margin for the first and the last items of the list:
/* This margin is supposed to "collapse" with the outer ones
but requires a fix below to work correctly with `VirtualScroller`. */
.list-item {
margin: 10px;
}
/* Fixes margin "collapse" for the first item. */
.list-item:first-child {
margin-top: 0;
}
/* Fixes margin "collapse" for the last item. */
.list-item:last-child {
margin-top: 0;
}
When styling the first and the last items of the list via :first-child
and :last-child
one should also check that such styles don't change the item's height which means that one should not add any border
or padding
styles to :first-child
and :last-child
otherwise the list items will jump by that extra height while scrolling.
An example of a :first-child
/:last-child
style that will not work correctly with VirtualScroller
:
.list-item {
border-bottom: 1px solid black;
}
.list-item:first-child {
border-top: 1px solid black;
}
Due to the inherent limitations of the <tbody/>
HTML tag, when used as a container for the list items, a workaround involving CSS variables has to be used, and CSS variables aren't supported in Internet Explorer, so using a <tbody/>
as a list items container won't work in Internet Explorer: in such case, VirtualScroller
renders in "bypass" mode (render all items).
Due to offscreen list items not being rendered native browser features like "Find on page", moving focus through items via Tab
key, screen reader announcement and such won't work. A workaround for "search on page" is adding a custom "🔍 Search" input field that would filter items by their content and then call VirtualScroller.setItems()
.
VirtualScroller
calculates the shown item indexes when its .onMount()
method is called, but if the page styles are applied after VirtualScroller
is mounted (for example, if styles are applied via javascript, like Webpack does it in dev mode with its style-loader
) then the list might not render correctly and will only show the first item. The reason for that is because calling .getBoundingClientRect()
on the list container DOM element on mount returns "incorrect" top
position because the styles haven't been applied yet, and so VirtualScroller
thinks it's offscreen.
For example, consider a page:
<div class="page">
<nav class="sidebar">...</nav>
<main>...</main>
</div>
The sidebar is styled as position: fixed
, but until the page styles have been applied it's gonna be a regular <div/>
meaning that <main/>
will be rendered below the sidebar causing it to be offscreen and so the list will only render the first item. Then, the page styles are loaded and applied and the sidebar is now position: fixed
so <main/>
is now rendered at the top of the page but VirtualScroller
's .render()
has already been called and it won't re-render until the user scrolls or the window is resized.
This type of a bug doesn't occur in production, but it can appear in development mode when using Webpack. The workaround VirtualScroller
implements for such cases is calling .getBoundingClientRect()
on the list container DOM element periodically (every second) to check if the top
coordinate has changed as a result of CSS being applied: if it has then it recalculates the shown item indexes and re-renders.
Set window.VirtualScrollerDebug
to true
to output debug messages to console
.
One can use any npm CDN service, e.g. unpkg.com or jsdelivr.net
<!-- Core. -->
<script src="https://unpkg.com/[email protected]/bundle/virtual-scroller.js"></script>
<script>
new VirtualScroller(...)
</script>
<!-- DOM component. -->
<script src="https://unpkg.com/[email protected]/bundle/virtual-scroller-dom.js"></script>
<script>
new VirtualScroller(...)
</script>
<!-- React component. -->
<script src="https://unpkg.com/[email protected]/bundle/virtual-scroller-react.js"></script>
<script>
<VirtualScroller .../>
</script>
On March 9th, 2020, GitHub, Inc. silently banned my account (erasing all my repos, issues and comments) without any notice or explanation. Because of that, all source codes had to be promptly moved to GitLab. GitHub repo is now deprecated, and the latest source codes can be found on GitLab, which is also the place to report any issues.