npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lacspace/virtual

v1.1.0

Published

Headless list virtualization for React — render only the rows in view, with fixed or dynamically-measured sizes, overscan, sticky/pinned rows, horizontal lists, and scroll-to-index. useVirtualizer in ~2KB, plus a React-free math core. Zero-dependency, SSR

Readme

@lacspace/virtual

Headless list virtualization for React — render 100k rows smoothly.

npm version install size minzipped types license

useVirtualizer mounts only the rows in view, so a list of any size stays smooth. Fixed or dynamically-measured sizes, overscan, gaps, horizontal lists, sticky rows and scroll-to-index — in ~2 KB. You own the markup; the hook owns the math.

  • 📜 Render 100k+ rows — only the visible window (plus overscan) is mounted
  • 📏 Fixed or dynamically-measured item sizes (measureElement + data-index)
  • ↔️ Vertical or horizontal · gaps · padding · scrollMargin for window scrolling
  • 📌 Sticky / pinned indices — keep section headers rendered while scrolled away
  • 🎯 scrollToIndex / scrollToOffset with alignment and smooth behaviour
  • 🧮 A React-free math core (calculateRange, scrollToIndexOffset, …) — testable and usable without a DOM
  • ⚡ ~2 KB · headless (no components/CSS) · 🌍 SSR-safe · 📦 ESM + CJS · fully typed

New in 1.1.0 — stickyIndices to keep pinned rows (headers) mounted while out of view, and the whole geometry is now exported as a pure, React-free core — calculateRange, buildMeasurements, computeRange, scrollToIndexOffset, augmentRangeWithSticky — so you can run the exact virtualization math in a worker, on the server, or in a plain unit test with no DOM. Fully backward compatible; every existing export is unchanged.

Install

npm install @lacspace/virtual

React >=18 is a peer dependency.

Usage

A complete vertical list. A scroll container holds an inner spacer sized to the full list; each visible row is absolutely positioned via translateY(item.start). Adding data-index + ref={measureElement} opts each row into dynamic height measurement (drop them for fixed heights).

import { useRef } from "react";
import { useVirtualizer } from "@lacspace/virtual";

function BigList({ rows }: { rows: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 44, // best guess before measuring
    overscan: 8,
  });

  return (
    <div ref={parentRef} style={{ height: 480, overflow: "auto" }}>
      {/* inner spacer: full scrollable size */}
      <div
        style={{
          height: virtualizer.getTotalSize(),
          position: "relative",
          width: "100%",
        }}
      >
        {virtualizer.getVirtualItems().map((item) => (
          <div
            key={item.key}
            data-index={item.index}
            ref={virtualizer.measureElement}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              transform: `translateY(${item.start}px)`,
            }}
          >
            {rows[item.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Fixed sizes

If every row is the same known height, drop data-index and measureElement and return that height from estimateSize — the layout is exact with zero measurement work.

Horizontal lists

Pass horizontal: true and swap the axis in your styles (width from getTotalSize(), transform: translateX(${item.start}px)).

Scroll to an item

virtualizer.scrollToIndex(5000, { align: "center", behavior: "smooth" });
virtualizer.scrollToOffset(0, { behavior: "smooth" }); // back to top

Window / document scrolling

When the whole page scrolls and the list starts partway down, point getScrollElement at the scrolling element and set scrollMargin to the list's distance from the top of the scrollable content.

Sticky / pinned rows

Pass stickyIndices to keep certain rows mounted even when they scroll out of view — perfect for section headers. They are always included in getVirtualItems() (in ascending order, never duplicated), so you can render them with position: sticky while every other row stays windowed.

const virtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 40,
  stickyIndices: headerIndices, // e.g. [0, 12, 30]
});

React-free math core

All the geometry is exported as pure functions with no React and no DOM, so you can compute layouts in a worker or on the server, or unit-test them directly:

import { calculateRange, scrollToIndexOffset } from "@lacspace/virtual";

const { virtualItems, totalSize, range } = calculateRange({
  count: 100_000,
  estimateSize: (i) => (i % 2 ? 32 : 48), // variable sizes
  scrollOffset: 12_000, // scrollTop (or scrollLeft when horizontal)
  containerSize: 600, // clientHeight (or clientWidth when horizontal)
  overscan: 5,
  stickyIndices: [0],
  measured: new Map([[3, 80]]), // measured-size overrides
});

// Where to scroll to reveal an item (start | center | end | auto):
const offset = scrollToIndexOffset(calculateRange(/* … */).items, 5000, {
  align: "center",
  containerSize: 600,
});

The useVirtualizer hook is a thin state/effects wrapper around this core, so it and the exported functions always agree.

API

useVirtualizer(options): Virtualizer

VirtualizerOptions

| Option | Type | Default | Description | | ------------------ | -------------------------------------- | ------- | ------------------------------------------------------------------ | | count | number | — | Total number of items. | | getScrollElement | () => HTMLElement \| null | — | Returns the scroll container (re-resolved each render). | | estimateSize | (index: number) => number | — | Estimated (or fixed) size in px along the scroll axis. | | overscan | number | 5 | Extra items rendered on each side of the viewport. | | horizontal | boolean | false | Lay out along the x-axis (left/width, scrollLeft). | | gap | number | 0 | Gap in px between items. | | getItemKey | (index: number) => number \| string | index | Stable key per item. | | paddingStart | number | 0 | Leading padding before the first item (counts toward total size). | | scrollMargin | number | 0 | Offset from scroll-content start to the list start. | | stickyIndices | number[] | [] | Indices always kept in getVirtualItems() even when out of view. |

Virtualizer

| Member | Type | Description | | ------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | getVirtualItems()| () => VirtualItem[] | Items currently in view, plus overscan. | | getTotalSize() | () => number | Full list size (px) for the inner spacer. | | scrollToIndex() | (index, { align?: "start"\|"center"\|"end"\|"auto"; behavior? }) => void | Scroll an item into view. | | scrollToOffset() | (offset, { behavior? }) => void | Scroll to an absolute px offset. | | measureElement() | (el: HTMLElement \| null) => void | Ref callback for dynamic sizing (needs data-index on the node). | | range | { startIndex: number; endIndex: number } \| null | The rendered index range (incl. overscan). | | options | VirtualizerOptions | The options passed in. |

VirtualItem

interface VirtualItem {
  index: number; // position in the source list
  start: number; // leading edge offset (px), relative to the inner container
  size: number;  // measured or estimated size (px)
  end: number;   // start + size
  key: number | string;
}

Pure core (React-free)

These are exported alongside the hook and use no React and no DOM — ideal for tests, workers or SSR.

| Function | Description | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------- | | calculateRange(input): CalculateRangeResult | Whole pipeline: { items, totalSize, range, virtualItems, stickyItems } from scroll + size.| | buildMeasurements(count, estimateSize, measured, paddingStart, gap, getItemKey?) | The cumulative offset/size table (VirtualItem[]). | | computeRange(items, scrollOffset, viewport, overscan, scrollMargin) | The visible index range (incl. overscan), or null. | | findStartIndex(items, offset) | Binary search for the first item at a scroll offset. | | getTotalSize(items) | Total list size (px). | | scrollToIndexOffset(items, index, { align, containerSize, scrollMargin?, currentOffset? }) | Target scroll offset to reveal an item, or null. | | augmentRangeWithSticky(range, stickyIndices, count) | Range indices unioned with sticky indices (ascending, de-duped). | | normalizeStickyIndices(stickyIndices, count) | Sort/de-dupe/clamp a sticky-index list. |

calculateRange accepts { count, estimateSize, scrollOffset, containerSize, overscan?, paddingStart?, gap?, scrollMargin?, measured?, stickyIndices?, getItemKey? }. The math is axis-agnostic, so horizontal lists pass scrollLeft/clientWidth.

Why it's tiny

  • No components, no CSS. It returns numbers; you render whatever you like.
  • Zero dependencies. Pure React hooks and the platform (ResizeObserver, getBoundingClientRect, scrollTo).
  • Cheap math. A cumulative-offset table is built once per data change and the visible window is found with a binary search — item lookups don't scan the list.
  • SSR-safe. No DOM access during render; measurement happens only in effects. On the server you get an estimated layout, so markup matches on hydration.
  • No render loops. State updates only fire when a value actually changes, and measured sizes are cached, so re-measuring a stable row is a no-op.

Licensing

Free under the Lacspace Free Licence — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice. See the Lacspace Licence Centre.


The Lacspace Developer Platform

@lacspace/virtual is part of 80+ zero-dependency, isomorphic TypeScript packages — one standard library for the modern web. Explore the ecosystem:

  • 📦 This package, documented — https://developer.lacspace.com/packages/virtual
  • 🗂️ All 80+ packages — https://developer.lacspace.com/packages
  • 🧭 Developer handbook — guides & runnable recipes — https://developer.lacspace.com/handbook
  • 🧪 Live playground — run any package in your browser — https://developer.lacspace.com/playground
  • 🖥️ Finished app templates — https://templates.lacspace.com
  • 🚀 Scaffold a full app — npm create lacspace-app@latest

Free under the Lacspace Free Licence — a permissive, free-to-use licence.