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

@floor/vlist

v0.8.2

Published

Lightweight, high-performance virtual list with zero dependencies

Readme

vlist

Lightweight, high-performance virtual list with zero dependencies and optimal tree-shaking.

npm version bundle size tests license

  • Zero dependencies — no external libraries
  • Ultra memory efficient — ~0.1-0.2 MB constant overhead regardless of dataset size
  • 8–12 KB gzipped — pay only for features you use (vs 20 KB+ monolithic alternatives)
  • Builder API — composable plugins with perfect tree-shaking
  • Grid, sections, async, selection, scale — all opt-in
  • Horizontal, reverse, page-scroll, wrap — every layout mode
  • Accessible — WAI-ARIA, keyboard navigation, screen-reader friendly
  • React, Vue, Svelte — framework adapters available

30+ interactive examples → vlist.dev

Installation

npm install @floor/vlist

Quick Start

import { vlist } from '@floor/vlist'
import '@floor/vlist/styles'

const list = vlist({
  container: '#my-list',
  items: [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' },
  ],
  item: {
    height: 48,
    template: (item) => `<div>${item.name}</div>`,
  },
}).build()

list.scrollToIndex(10)
list.setItems(newItems)
list.on('item:click', ({ item }) => console.log(item))

Builder Pattern

Start with the base, add only what you need:

import { vlist, withGrid, withSections, withSelection } from '@floor/vlist'

const list = vlist({
  container: '#app',
  items: photos,
  item: { height: 200, template: renderPhoto },
})
  .use(withGrid({ columns: 4, gap: 16 }))
  .use(withSections({
    getGroupForIndex: (i) => photos[i].category,
    headerHeight: 40,
    headerTemplate: (cat) => `<h2>${cat}</h2>`,
  }))
  .use(withSelection({ mode: 'multiple' }))
  .build()

Plugins

| Plugin | Size | Description | |--------|------|-------------| | Base | 7.7 KB | Core virtualization | | withGrid() | +4.0 KB | 2D grid layout | | withSections() | +4.6 KB | Grouped lists with sticky/inline headers | | withAsync() | +5.3 KB | Lazy loading with adapters | | withSelection() | +2.3 KB | Single/multiple selection + keyboard nav | | withScale() | +2.2 KB | 1M+ items via scroll compression | | withScrollbar() | +1.0 KB | Custom scrollbar UI | | withPage() | +0.9 KB | Document-level scrolling | | withSnapshots() | included | Scroll save/restore |

Examples

More examples at vlist.dev.

Grid Layout

import { vlist, withGrid, withScrollbar } from '@floor/vlist'

const gallery = vlist({
  container: '#gallery',
  items: photos,
  item: {
    height: 200,
    template: (photo) => `
      <div class="card">
        <img src="${photo.url}" />
        <span>${photo.title}</span>
      </div>
    `,
  },
})
  .use(withGrid({ columns: 4, gap: 16 }))
  .use(withScrollbar({ autoHide: true }))
  .build()

Sticky Headers

import { vlist, withSections } from '@floor/vlist'

const contacts = vlist({
  container: '#contacts',
  items: sortedContacts,
  item: {
    height: 56,
    template: (contact) => `<div>${contact.name}</div>`,
  },
})
  .use(withSections({
    getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
    headerHeight: 36,
    headerTemplate: (letter) => `<div class="header">${letter}</div>`,
    sticky: true,
  }))
  .build()

Set sticky: false for inline headers (iMessage/WhatsApp style).

Async Loading

import { vlist, withAsync } from '@floor/vlist'

const list = vlist({
  container: '#list',
  item: {
    height: 64,
    template: (item) => item
      ? `<div>${item.name}</div>`
      : `<div class="placeholder">Loading…</div>`,
  },
})
  .use(withAsync({
    adapter: {
      read: async ({ offset, limit }) => {
        const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
        const data = await res.json()
        return { items: data.items, total: data.total, hasMore: data.hasMore }
      },
    },
  }))
  .build()

More Patterns

| Pattern | Key options | |---------|------------| | Chat UI | reverse: true + withSections({ sticky: false }) | | Horizontal carousel | direction: 'horizontal', item.width | | Page-level scroll | withPage() | | 1M+ items | withScale() — auto-compresses scroll space | | Wrap navigation | scroll: { wrap: true } | | Variable heights | item: { height: (index) => heights[index] } |

See vlist.dev for live demos of each.

API

const list = vlist(config).use(...plugins).build()

Data

| Method | Description | |--------|-------------| | list.setItems(items) | Replace all items | | list.appendItems(items) | Add to end (auto-scrolls in reverse mode) | | list.prependItems(items) | Add to start (preserves scroll position) | | list.updateItem(index, partial) | Update a single item by index | | list.removeItem(index) | Remove by index | | list.reload() | Re-fetch from adapter (async) |

Navigation

| Method | Description | |--------|-------------| | list.scrollToIndex(i, align?) | Scroll to index ('start' | 'center' | 'end') | | list.scrollToIndex(i, opts?) | With { align, behavior: 'smooth', duration } | | list.cancelScroll() | Cancel smooth scroll animation | | list.getScrollPosition() | Current scroll offset | | list.getVisibleRange() | { start, end } of visible indices | | list.getScrollSnapshot() | Save scroll state (for SPA navigation) | | list.restoreScroll(snapshot) | Restore saved scroll state |

Selection (with withSelection())

| Method | Description | |--------|-------------| | list.selectItem(id) | Select item | | list.deselectItem(id) | Deselect item | | list.toggleSelection(id) | Toggle | | list.selectAll() / list.clearSelection() | Bulk operations | | list.getSelectedIds() | Array of selected IDs | | list.getSelectedItems() | Array of selected items |

Grid (with withGrid())

| Method | Description | |--------|-------------| | list.updateGrid({ columns, gap }) | Update grid at runtime |

Events

list.on() returns an unsubscribe function. You can also use list.off(event, handler).

list.on('scroll', ({ scrollTop, direction }) => {})
list.on('range:change', ({ range }) => {})
list.on('item:click', ({ item, index, event }) => {})
list.on('item:dblclick', ({ item, index, event }) => {})
list.on('selection:change', ({ selectedIds, selectedItems }) => {})
list.on('load:start', ({ offset, limit }) => {})
list.on('load:end', ({ items, offset, total }) => {})
list.on('load:error', ({ error, offset, limit }) => {})
list.on('velocity:change', ({ velocity, reliable }) => {})

Properties

| Property | Description | |----------|-------------| | list.element | Root DOM element | | list.items | Current items (readonly) | | list.total | Total item count |

Lifecycle

list.destroy()

Plugin Configuration

Each plugin's config is fully typed — hover in your IDE for details.

withGrid({ columns: 4, gap: 16 })
withSections({ getGroupForIndex, headerHeight, headerTemplate, sticky?: true })
withSelection({ mode: 'single' | 'multiple', initial?: [...ids] })
withAsync({ adapter: { read }, loading?: { cancelThreshold? } })
withScale()                           // no config — auto-activates at 16.7M px
withScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
withPage()                            // no config — uses document scroll
withSnapshots()                       // included by default

Full configuration reference → vlist.dev

Framework Adapters

| Framework | Package | Size | |-----------|---------|------| | React | vlist-react | 0.6 KB gzip | | Vue | vlist-vue | 0.6 KB gzip | | Svelte | vlist-svelte | 0.5 KB gzip |

npm install @floor/vlist vlist-react   # or vlist-vue / vlist-svelte

Each adapter README has setup examples and API docs.

Styling

import '@floor/vlist/styles'           // base styles (required)
import '@floor/vlist/styles/extras'    // optional enhancements

Override with your own CSS using the .vlist, .vlist-item, .vlist-item--selected, .vlist-scrollbar selectors. See vlist.dev for theming examples.

Performance

Bundle Size

| Configuration | Gzipped | |---------------|---------| | Base only | 7.7 KB | | + Grid | 11.7 KB | | + Sections | 12.3 KB | | + Async | 13.5 KB | | All plugins | ~16 KB |

Memory Efficiency

vlist uses constant memory regardless of dataset size through optimized internal architecture:

| Dataset Size | Memory Usage | Notes | |--------------|--------------|-------| | 10K items | ~0.2 MB | Constant baseline | | 100K items | ~0.2 MB | 10× items, same memory | | 1M items | ~0.4 MB | 100× items, 2× memory |

Key advantages:

  • No array copying — uses references for zero-copy performance
  • No ID indexing overhead — O(1) memory complexity
  • Industry-leading memory efficiency for virtual list libraries

DOM Efficiency

With 100K items: ~26 DOM nodes in the document (visible + overscan) instead of 100,000.

Render Performance

  • Initial render: ~8ms (constant, regardless of item count)
  • Scroll performance: 120 FPS (perfect smoothness)
  • 1M items: Same performance as 10K items

TypeScript

Fully typed. Generic over your item type:

import { vlist, withGrid, type VList } from '@floor/vlist'

interface Photo { id: number; url: string; title: string }

const list: VList<Photo> = vlist<Photo>({
  container: '#gallery',
  items: photos,
  item: {
    height: 200,
    template: (photo) => `<img src="${photo.url}" />`,
  },
})
  .use(withGrid({ columns: 4 }))
  .build()

Contributing

  1. Fork → branch → make changes → add tests → pull request
  2. Run bun test (1739 tests) and bun run build before submitting

License

MIT

Links


Built by Floor IO