Joshua Amaju - How I built a virtual scroll custom element
← Back to writing

How I built a virtual scroll custom element

The problem

If you’ve ever rendered a large list in the browser, you’ve probably hit the point where performance starts to fall apart. Too many DOM nodes means slower rendering, more layout work, and a visibly worse scrolling experience.

That is usually when you reach for virtualization. There are already great tools for this, like TanStack Virtual, but most solutions are tied to a framework, a rendering model, or a very specific layout strategy.

For a problem this common, I kept wondering why there was no built-in HTML element for it.

Another thing kept bothering me: many virtual scroll libraries rely on absolute positioning. That works, but it also means you end up authoring your markup and CSS around the library instead of writing normal layout code.

I wanted the opposite. I wanted virtualization to fit into normal HTML and CSS, not force everything around it to change.

The idea

The goal was simple: build an element that behaves as much like a regular scroll container as possible.

Not a React hook. Not a framework adapter. Just an element you can drop in anywhere.

Something that feels like this:

<ul style="overflow: auto;">
  <li>Element</li>
  <li>Element</li>
  <li>Element</li>
</ul>

But virtualized.

There was actually a now-abandoned WICG proposal for a browser-native virtual scroller. One useful thing that came out of that broader work was content-visibility, which helped validate that the platform was already moving in this direction.

That is what pushed me to build @valaria/virtual-scroll: a custom element that aims to be a drop-in replacement for any scrollable container.

The API

The API is intentionally boring. You take markup like this:

<ul>
  <li>Element</li>
  <li>Element</li>
  <li>Element</li>
</ul>

And turn it into this:

<virtual-scroll>
  <li>Element</li>
  <li>Element</li>
  <li>Element</li>
</virtual-scroll>

No fixed width.

No fixed height.

No absolute positioning.

No transform-based layout tricks.

position: sticky works as expected.

It works with any framework.

Most importantly, it behaves like a normal HTML element almost all the time. That was the bar I cared about most.

The trade-off

Of course, virtualization always comes with trade-offs.

If only part of a list is mounted, then some DOM assumptions stop being true. Selectors and APIs that depend on every sibling existing at once can become unreliable. For example, selectors like :nth-child() are not always meaningful when most of the list is intentionally not in the DOM.

So the goal was not to pretend virtualization has no edge cases. The goal was to make those edge cases rare, and let everything else feel as native as possible.


I had a lot of fun building this. If you’ve ever wanted virtualization without redesigning your layout around it, I think you’ll like it.