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

sogna-virtual-list

v0.1.1

Published

A lightweight React virtual list tuned for chat, feeds, and dynamic-height message UIs.

Readme

sogna-virtual-list

A lightweight React virtual list for chat, feeds, logs, and dynamic-height message UIs.

sogna-virtual-list keeps large, frequently changing lists responsive while preserving scroll position during common messaging workflows: appending new items, prepending history, streaming content into an existing row, trimming old items, and jumping to a known item.

Features

  • Dynamic item measurement with ResizeObserver.
  • Scroll-position repair for prepend, append, trim, replace, and streaming updates.
  • Imperative data and scroll methods through a forwarded ref or hook.
  • Header, footer, sticky header, sticky footer, empty state, and custom scroller slots.
  • TypeScript declarations generated from the source.
  • No runtime dependency beyond React and React DOM.

Install

npm install sogna-virtual-list

React and React DOM are peer dependencies:

npm install react react-dom

Requirements

  • React 18 or React 19.
  • Node.js 18 or newer for local development and builds.
  • A browser environment with ResizeObserver.

useWindowScroll and customScrollParent are not implemented in 0.1.0. Passing either prop falls back to the internal scroller and logs a warning.

Basic Usage

The default flow is top-down: the first message starts at the top and new messages grow downward.

import { SognaVirtualList } from "sogna-virtual-list";

type Message = {
  id: string;
  author: "user" | "assistant";
  text: string;
};

export function ChatList({ messages }: { messages: Message[] }) {
  return (
    <SognaVirtualList<Message, null>
      data={{
        data: messages,
      }}
      context={null}
      computeItemKey={({ data }) => data.id}
      ItemContent={({ data }) => (
        <div style={{ padding: "6px 12px" }}>
          <div style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
            {data.text}
          </div>
        </div>
      )}
      style={{ height: 480, width: "100%" }}
    />
  );
}

For a classic chat window where short conversations sit at the bottom and the initial view opens on the latest item, use messageFlow="bottom-up".

<SognaVirtualList<Message, null>
  data={{ data: messages }}
  messageFlow="bottom-up"
  context={null}
  computeItemKey={({ data }) => data.id}
  ItemContent={({ data }) => <div>{data.text}</div>}
  style={{ height: 480 }}
/>

Streaming Updates

For streaming text, keep the same item id and update that item with scrollModifier: { type: "items-change", behavior: "auto" }. If the user is already at the bottom, the list follows the growing item. If the user has scrolled up, their reading position is preserved.

import { SognaVirtualList } from "sogna-virtual-list";
import type { DataWithScrollModifier } from "sogna-virtual-list";

type Message = {
  id: string;
  role: "user" | "assistant";
  content: string;
};

function StreamingChat({ messages }: { messages: Message[] }) {
  const data: DataWithScrollModifier<Message> = {
    data: messages,
    scrollModifier: {
      type: "items-change",
      behavior: "auto",
    },
  };

  return (
    <SognaVirtualList<Message, null>
      data={data}
      context={null}
      computeItemKey={({ data }) => data.id}
      ItemContent={({ data }) => <div>{data.content}</div>}
      style={{ height: "100%" }}
    />
  );
}

Loading Older Items

Use the prepend scroll modifier when older items are inserted before the current data. The visible viewport stays anchored instead of jumping.

const data = {
  data: [...olderMessages, ...messages],
  scrollModifier: "prepend" as const,
};

Imperative API

Use a ref when the owner component needs to control the list:

import { createRef } from "react";
import {
  SognaVirtualList,
  type SognaVirtualListMethods,
} from "sogna-virtual-list";

const ref = createRef<SognaVirtualListMethods<Message, null>>();

<SognaVirtualList<Message, null>
  ref={ref}
  data={{ data: messages }}
  context={null}
  ItemContent={({ data }) => <div>{data.text}</div>}
/>;

ref.current?.scrollToItem({ index: "LAST", align: "end", behavior: "smooth" });

Use useSognaVirtualListMethods from inside the list tree:

import { useSognaVirtualListMethods } from "sogna-virtual-list";

function AddMessageButton() {
  const methods = useSognaVirtualListMethods<Message, null>();

  return (
    <button
      type="button"
      onClick={() => {
        methods.data.append(
          [{ id: crypto.randomUUID(), role: "assistant", content: "New item" }],
          "smooth",
        );
      }}
    >
      Add
    </button>
  );
}

Data methods:

  • append(data, autoscrollToBottom?)
  • prepend(data)
  • insert(data, offset, autoscrollToBottom?)
  • deleteRange(offset, count)
  • find(predicate)
  • findIndex(predicate)
  • findAndDelete(predicate)
  • map(callback, autoscrollToBottomBehavior?)
  • mapWithAnchor(callback, anchorItemIndex)
  • replace(data, options?)
  • batch(callback, autoscrollToBottom?)
  • removeFromStart(count)
  • get()
  • getCurrentlyRendered()

Scroll methods:

  • scrollToItem(location)
  • scrollIntoView(location)
  • getScrollLocation()
  • scrollerElement()
  • cancelSmoothScroll()
  • height(item)

Scroll Modifiers

scrollModifier tells the list how to repair scroll position after data changes.

type ScrollModifier =
  | "prepend"
  | "remove-from-start"
  | "remove-from-end"
  | {
      type: "item-location";
      location: ItemLocation;
      purgeItemSizes?: boolean;
    }
  | {
      type: "auto-scroll-to-bottom";
      autoScroll: AutoscrollToBottom;
    }
  | {
      type: "items-change";
      behavior: ScrollBehavior | { location: () => ItemLocation | null };
    };

Common choices:

  • "prepend" for loading older items above the viewport.
  • "remove-from-start" for trimming old items from the beginning.
  • { type: "items-change", behavior: "auto" } for streaming updates.
  • { type: "item-location", location: { index: "LAST", align: "end" } } for replacing data and jumping to the latest item.
  • { type: "auto-scroll-to-bottom", autoScroll: true } for appending only when the user is already at the bottom.

Props

The main component is generic:

<SognaVirtualList<Data, Context> />

Core props:

  • data: controlled data with an optional scrollModifier.
  • initialData: uncontrolled initial data.
  • context: arbitrary value passed to render slots.
  • messageFlow: "top-down" by default. Use "bottom-up" for classic bottom-aligned chat behavior.
  • computeItemKey: stable key resolver.
  • ItemContent: item renderer.
  • Header, StickyHeader, Footer, StickyFooter, EmptyPlaceholder: optional slot components.
  • ScrollElement: custom scroll element component or "div".
  • onScroll: receives the current ListScrollLocation.
  • onRenderedDataChange: receives the currently rendered data range.
  • shortSizeAlign: "top", "bottom", or "bottom-smooth". Overrides the alignment implied by messageFlow.
  • increaseViewportBy: extra pixels rendered above and below the viewport.
  • itemIdentity: identity resolver used by item-size cache logic.
  • enforceStickyFooterAtBottom: keeps the sticky footer pinned when possible.

Testing

SognaVirtualListTestingContext lets tests provide deterministic viewport and item sizes.

import {
  SognaVirtualList,
  SognaVirtualListTestingContext,
} from "sogna-virtual-list";

render(
  <SognaVirtualListTestingContext.Provider
    value={{ viewportHeight: 400, itemHeight: 40 }}
  >
    <SognaVirtualList
      data={{ data: messages }}
      context={null}
      ItemContent={({ data }) => <div>{data.text}</div>}
    />
  </SognaVirtualListTestingContext.Provider>,
);

Development

npm install
npm run typecheck
npm run test
npm run build
npm run pack:dry-run

Run the full local validation before publishing:

npm run validate

Publishing

This package is configured for public npm publishing.

npm login
npm version patch
npm publish --access public

Repository:

[email protected]:Victor-ChanX/sogna-virtual-list.git

License And Notices

MIT. See LICENSE.

Some internal engine primitives under src/engine are copied or adapted from MIT-licensed react-virtuoso. See NOTICE.md and THIRD_PARTY_LICENSES.md.