Skip to content

Repository files navigation

vlist

The virtual list library for every framework. Ultra efficient, batteries-included, and accessible with composable plugins — in 9.7 KB.

v2.6.1Changelog · Grid PageUp/PageDown now preserve the column at the top/bottom row instead of jumping to the corner (#60).

npm versionbundle sizeCIlicense

  • Accessible — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering
  • Zero dependencies — framework-agnostic core with tiny adapters for Vue, Svelte, Solid, React
  • 9.7 KB gzipped — composable plugins with perfect tree-shaking
  • Constant memory — ~0.1 MB overhead at any scale, from 10K to 1M+ items
  • Tree, grid, masonry, carousel, table, groups, data, selection, search, sortable, transition — all opt-in
  • Axis-neutral — vertical and horizontal scrolling through a single code path, all plugins work in both orientations

18 interactive examples, docs & benchmarks → vlist.io

Why vlist

vlistTanStack Virtualreact-virtuosovirtuavue-virtual-scroller
A11y built-inWAI-ARIA + 2D keyboardNone (DIY)PartialMinimalNone
Grid + Masonry + TableAllGrid onlyGrid + TableGrid onlyNone
Vue0.6 KB adapterYesYes11.8 KB
Svelte0.5 KB adapterYesYes
Solid0.5 KB adapterYesYes
Vanilla JSNativeYes
Constant memory~0.1 MB at 1MNoNoNoNo

Framework Adapters

FrameworkPackageSize
Vanilla JSvlistNative — no adapter needed
Vuevlist-vue0.6 KB gzip
Sveltevlist-svelte0.5 KB gzip
SolidJSvlist-solidjs0.5 KB gzip
Reactvlist-react0.6 KB gzip
npm install vlist # vanilla JS
npm install vlist vlist-vue # or vlist-svelte / vlist-solidjs / vlist-react

Quick Start

import{createVList}from'vlist'import'vlist/styles'constlist=createVList({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>`,},})list.scrollToIndex(10)list.setItems(newItems)list.on('item:click',({ item })=>console.log(item))

Plugin System

Start with the base, add only what you need:

import{createVList,grid,groups,selection}from'vlist'constlist=createVList({container: '#app',items: photos,item: {height: 200,template: renderPhoto},},[grid({columns: 4,gap: 16}),groups({getGroupForIndex: (i)=>photos[i].category,header: {height: 40,template: (cat)=>`<h2>${cat}</h2>`},}),selection({mode: 'multiple'}),])

Plugins

PluginSizeDescription
Base9.7 KBVirtualization, ARIA, keyboard nav, gap, padding, bounded scroll (1M+ items)
data()+4.8 KBLazy loading with velocity-aware fetching
selection()+2.7 KBSingle/multiple selection with 2D keyboard nav
search()+3.2 KBSearch bar: filter/navigate modes, match highlighting
groups()+5.3 KBSticky/inline headers with grid + masonry + table + data integration
autosize()+0.8 KBAuto-measure items via ResizeObserver
scrollbar()+2.0 KBCustom scrollbar UI
grid()+2.4 KB2D grid layout
masonry()+4.0 KBPinterest-style masonry with lane-aware keyboard nav
carousel()+3.4 KBPaged horizontal carousel with snap and keyboard nav
table()+5.8 KBData table with columns, resize, sort
tree()+5.0 KBCollapsible tree with async loading and indent guides
page()+0.8 KBWindow-level scrolling
sortable()+3.0 KBDrag-and-drop reordering with auto-scroll
snapshots()+1.1 KBScroll position save/restore
transition()+1.8 KBFLIP-based enter/exit animations for insert & remove

Examples

More examples at vlist.io.

Data Table

import{createVList,table,selection}from'vlist'constmyTable=createVList({container: '#my-table',items: contacts,item: {height: 36,template: ()=>''},},[table({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,}),selection({mode: 'single'}),])myTable.on('column:sort',({ key, direction })=>{/* re-sort data */})myTable.on('column:resize',({ key, width })=>{/* persist widths */})

Grid Layout

import{createVList,grid,scrollbar}from'vlist'constgallery=createVList({container: '#gallery',items: photos,item: {height: 200,template: (photo)=>` <div class="card"> <img src="${photo.url}" /> <span>${photo.title}</span> </div> `,},},[grid({columns: 4,gap: 16}),scrollbar({autoHide: true}),])

Animated Insert & Remove

import{createVList,transition,selection}from'vlist'constlist=createVList({container: '#playlist',items: tracks,item: {height: 64,template: renderTrack},},[transition({duration: 200}),selection({mode: 'multiple'}),])// Single item — collapses with fade-out, siblings slide uplist.removeItem(trackId)// Batch — all items animate simultaneouslylist.removeItems(list.getSelected())// Insert — expands in, siblings slide downlist.insertItem({id: 42,title: 'New Track'},0)

Async Loading

import{createVList,data}from'vlist'constlist=createVList({container: '#list',item: {height: 64,template: (item)=>item
? `<div>${item.name}</div>`
: `<div class="placeholder">Loading…</div>`,},},[data({adapter: {read: async({ offset, limit })=>{constres=awaitfetch(`/api/users?offset=${offset}&limit=${limit}`)constdata=awaitres.json()return{items: data.items,total: data.total,hasMore: data.hasMore}},},}),])

Search

import{createVList,search}from'vlist'import'vlist/styles/search'constlist=createVList({container: '#app',items: people,item: {height: 48,template: renderPerson},},[// Zero-config: a search bar at the top, Ctrl/⌘+F to focus, type to filter,// matches highlighted with <mark>. Use mode: 'navigate' to jump between// matches instead of hiding non-matches.search(),])

Accessibility

Every vlist is accessible by default following the WAI-ARIA listbox pattern:

  • Arrow keys move focus between items with a visible focus ring
  • 2D navigation in grids and masonry — Up/Down by row, Left/Right by cell
  • Masonry lane-aware nav — arrows stay in the same visual column
  • Home/End, PageUp/PageDown, Ctrl+Home/End — full keyboard coverage
  • Screen-reader DOM ordering — items reordered on scroll idle for correct reading order
  • Focus recovery — maintains focus when items are removed

Set interactive: false for display-only lists (log viewers, activity feeds) where items contain their own interactive elements.

API

constlist=createVList(config,[plugin1(),plugin2()])

Data

MethodDescription
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(id, partial)Update a single item by ID
list.insertItem(item, index?)Insert at index (animated with transition)
list.removeItem(id)Remove by ID (animated with transition)
list.removeItems(ids)Batch remove (simultaneous animations)
list.getItemAt(index)Get item at index
list.getIndexById(id)Get index by item ID

Navigation

MethodDescription
list.scrollToIndex(i, align?)Scroll to index ('start' | 'center' | 'end')
list.scrollToIndex(i, opts?)With { align, behavior: 'smooth', duration }
list.getScrollPosition()Current scroll offset

Selection (with selection())

MethodDescription
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

Events

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

list.on('scroll',({ scrollPosition, 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('sort:end',({ fromIndex, toIndex })=>{})list.on('sort:cancel',({ originalItems })=>{})

Properties

PropertyDescription
list.elementRoot DOM element
list.itemsCurrent items (readonly)
list.totalTotal item count
list.destroy()Cleanup and remove from DOM

Plugin Configuration

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

grid({columns: 4,gap: 16})masonry({columns: 4,gap: 16})groups({ getGroupForIndex,header: { height, template },sticky?: true})selection({mode: 'single'|'multiple',initial?: [...ids]})data({adapter: { read },loading?: {cancelThreshold? }})table({ columns, rowHeight,headerHeight?,resizable? })autosize()// auto-measure items (requires estimatedHeight)scrollbar({autoHide?,autoHideDelay?,minThumbSize? })transition({duration?: 200,insert?: timing,remove?: timing})sortable({handle?: '.drag-handle'})// drag-and-drop reorderingpage()// no config — uses document scrollsnapshots({autoSave: 'key'})// automatic sessionStorage save/restore

Full configuration reference → vlist.io

Base Configuration

OptionDefaultDescription
overscan3Extra items rendered outside viewport
ariaLabelAccessible label for the listbox
orientation'vertical''vertical' or 'horizontal' scroll direction
padding0Content inset — number, [v, h], or [top, right, bottom, left]
interactivetrueEnable built-in keyboard navigation
reversefalseReverse mode for chat UIs

Styling

import'vlist/styles'// core (always required)import'vlist/styles/grid'// when using grid()import'vlist/styles/masonry'// when using masonry()import'vlist/styles/table'// when using table()import'vlist/styles/extras'// optional (variants, loading states, animations)

Dark mode works out of the box via prefers-color-scheme, Tailwind's .dark class, or data-theme-mode="dark". Override CSS custom properties to match your design system. See vlist.io/tutorials/styling for the full guide.

Performance

Dataset SizeAfter RenderScroll Delta
10K items0.07 MB~0 MB
100K items0.08 MB~0 MB
1M items0.09 MB0.19 MB
  • Initial render: ~2ms (constant, regardless of item count)
  • Scroll: 120 FPS at any scale
  • DOM nodes: ~26 in document with 100K items (visible + overscan only)

Live benchmarks against 9 competitors → vlist.io/benchmarks

TypeScript

Fully typed. Generic over your item type:

import{createVList,grid,typeVList}from'vlist'interfacePhoto{id: number;url: string;title: string}constlist: VList<Photo>=createVList<Photo>({container: '#gallery',items: photos,item: {height: 200,template: (photo)=>`<img src="${photo.url}" />`,},},[grid({columns: 4})])

Migrating from v1

v2 is a ground-up rewrite — simpler API, 55% smaller base bundle, zero-allocation scroll path. Full announcement →

v1v2
vlist(config).use(withGrid()).build()createVList(config, [grid()])
withGrid, withSelection, …grid, selection, …
VListFeatureVListPlugin
BuilderContextPluginContext
.vlist-items.vlist-content

The instance API (setItems, scrollToIndex, on, destroy) is unchanged.

Contributing

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

License

MIT

Links


Built by FloorIO

About

Accessible, batteries-included ultra efficient virtual list for every framework. Zero deps, 9.7 KB

Topics

Resources

Contributing

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages