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

v1.4.0

Published

Lightweight, high-performance virtual list with zero dependencies

Downloads

1,086

Readme

vlist

Lightweight, high-performance virtual list with zero dependencies and dimension-agnostic architecture.

v1.4.0Changelog

npm version CI license

  • Zero dependencies — no external libraries
  • Ultra memory efficient — ~0.1-0.2 MB constant overhead regardless of dataset size
  • ~9.9 KB gzipped — pay only for features you use (vs 20 KB+ monolithic alternatives)
  • Builder API — composable features with perfect tree-shaking
  • Grid, masonry, table, groups, async, selection, scale — all opt-in
  • Horizontal & vertical — semantically correct orientation support
  • Gap & padding — built-in item spacing and content inset (CSS shorthand convention)
  • Reverse, page-scroll, wrap — every layout mode
  • Accessible — WAI-ARIA, keyboard navigation, focus-visible, screen-reader DOM ordering, ARIA live region
  • React, Vue, Svelte — framework adapters available

14+ interactive examples → vlist.io

Highlights

  • Data table — virtualized columns with resize, sort, horizontal scroll, and grouped sections via withTable()
  • Dimension-agnostic API — semantically correct terminology for both orientations
  • Performance optimized — 13-pattern optimization playbook applied across the entire rendering pipeline
  • Horizontal groups — sticky headers work in horizontal carousels
  • Horizontal grid layouts — 2D grids work in both orientations
  • Masonry — shortest-lane placement via withMasonry()
  • Keyboard accessible — focus-visible outlines, arrow/Home/End navigation, Tab support
  • Responsive grid & masonry — context-injected columnWidth auto-recalculates on resize

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, withGroups, withSelection } from '@floor/vlist'

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

Features

| Feature | Size | Description | |---------|------|-------------| | Base | 9.9 KB | Core virtualization, gap, padding, ARIA live region | | withGrid() | +3.9 KB | 2D grid layout with context injection | | withMasonry() | +2.7 KB | Pinterest-style masonry layout | | withGroups() | +4.2 KB | Grouped lists with sticky/inline headers | | withAsync() | +4.4 KB | Lazy loading with adapters | | withSelection() | +1.7 KB | Single/multiple selection + keyboard nav | | withScale() | +3.0 KB | 1M+ items via scroll compression | | withScrollbar() | +1.1 KB | Custom scrollbar UI | | withTable() | +5.1 KB | Data table with columns, resize, sort, groups | | withPage() | +0.4 KB | Document-level scrolling | | withSnapshots() | +0.6 KB | Scroll save/restore |

Examples

More examples at vlist.io.

Data Table

import { vlist, withTable, withSelection } from '@floor/vlist'

const table = vlist({
  container: '#my-table',
  items: contacts,
  item: { height: 36, template: () => '' },
})
  .use(withTable({
    columns: [
      { key: 'name',   label: 'Name',   width: 200, sortable: true },
      { key: 'email',  label: 'Email',  width: 260, sortable: true },
      { key: 'role',   label: 'Role',   width: 160, sortable: true },
      { key: 'status', label: 'Status', width: 100, align: 'center' },
    ],
    rowHeight: 36,
    headerHeight: 36,
    resizable: true,
  }))
  .use(withSelection({ mode: 'single' }))
  .build()

table.on('column:sort', ({ key, direction }) => { /* re-sort data */ })
table.on('column:resize', ({ key, width }) => { /* persist widths */ })

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, withGroups } from '@floor/vlist'

const contacts = vlist({
  container: '#contacts',
  items: sortedContacts,
  item: {
    height: 56,
    template: (contact) => `<div>${contact.name}</div>`,
  },
})
  .use(withGroups({
    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 + withGroups({ sticky: false }) | | Horizontal carousel | orientation: 'horizontal', item.width | | Horizontal groups | orientation: 'horizontal' + withGroups() | | Horizontal grid | orientation: 'horizontal' + withGrid() | | Data table | withTable({ columns, rowHeight, resizable }) | | Grouped table | withTable({ columns, rowHeight }) + withGroups({ ... }) | | Item gap | item: { height: 48, gap: 8 } | | Content padding | padding: 16 or padding: [16, 12] or padding: [16, 12, 20, 8] | | Masonry | withMasonry({ columns: 4, gap: 16 }) | | Page-level scroll | withPage() | | 1M+ items | withScale() — auto-compresses scroll space | | Wrap navigation | scroll: { wrap: true } | | Variable heights | item: { height: (index) => heights[index] } | | Zebra striping | item: { striped: true } or striped: 'even' / 'odd' / 'data' (group-aware) |

See vlist.io for live demos of each.

API

const list = vlist(config).use(...features).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.getItemAt(index) | Get item at index | | list.getIndexById(id) | Get index by item ID | | list.reload() | Re-fetch from adapter (async) | | list.reload({ snapshot }) | Re-fetch and restore scroll position from snapshot |

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 |

Snapshots (with withSnapshots())

| Method | Description | |--------|-------------| | list.getScrollSnapshot() | Save scroll state (for SPA navigation) | | list.restoreScroll(snapshot) | Restore saved scroll state |

Selection (with withSelection())

| Method | Description | |--------|-------------| | list.select(...ids) | Select item(s) | | list.deselect(...ids) | Deselect item(s) | | list.toggleSelect(id) | Toggle | | list.selectAll() / list.clearSelection() | Bulk operations | | list.getSelected() | Array of selected IDs | | list.getSelectedItems() | Array of selected items |

Grid (with withGrid())

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

Table (with withTable())

| Method | Description | |--------|-------------| | list.setSort(key, direction?) | Set sort indicator (visual only) | | list.getSort() | Get current { key, direction } | | list.updateColumns(columns) | Replace column definitions at runtime | | list.resizeColumn(key, width) | Resize a column programmatically | | list.getColumnWidths() | Get current widths keyed by column key |

| Event | Payload | |-------|---------| | column:sort | { key, direction, index } | | column:resize | { key, width, previousWidth, index } | | column:click | { key, index, event } |

Events

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

list.on('scroll', ({ scrollPosition, direction }) => {})  // v0.9.0: scrollPosition (was scrollTop)
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 }) => {})

Statistics (with createStats())

| Method | Description | |--------|-------------| | createStats(list) | Create a stats tracker for scroll performance metrics |

Properties

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

Lifecycle

list.destroy()

Feature Configuration

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

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

Full configuration reference → vlist.io

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 | | SolidJS | vlist-solidjs | 0.5 KB gzip |

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

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.io for theming examples.

Dark Mode

Dark mode is supported out of the box via three mechanisms (no extra imports needed):

| Method | How it works | |--------|-------------| | OS preference | prefers-color-scheme: dark — automatic | | Tailwind .dark class | Add .dark to any ancestor element | | data-theme-mode | Set data-theme-mode="dark" on <html> for explicit control |

To force light mode when prefers-color-scheme would otherwise activate dark, set data-theme-mode="light" on the root element. All dark mode CSS variables and color-scheme declarations are handled automatically.

Architecture

Dimension-Agnostic Design (v0.9.0)

vlist uses semantically correct terminology that works for both vertical and horizontal orientations:

// ✅ Correct: Works for both orientations
sizeCache.getSize(index)       // Returns height OR width
state.scrollPosition           // scrollTop OR scrollLeft
state.containerSize            // height OR width

// Previously (v0.8.2): Semantically wrong in horizontal mode
heightCache.getHeight(index)   // ❌ Returned WIDTH in horizontal!
state.scrollTop                // ❌ Stored scrollLEFT!

This makes the codebase clearer and eliminates semantic confusion when working with horizontal lists.

Migration from v0.8.2: See v0.9.0 Migration Guide

Performance

Bundle Size

| Configuration | Gzipped | |---------------|---------| | Base only | 9.9 KB | | + Grid | 13.8 KB | | + Groups | 14.1 KB | | + Async | 14.3 KB | | + Table | 15.0 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 and bun run build before submitting

License

MIT

Changelog

See CHANGELOG.md for the full release history. A simplified changelog.txt is also available.

Links


Built by Floor IO