{"id":49486437,"url":"https://github.com/floor/vlist","last_synced_at":"2026-06-07T20:01:08.305Z","repository":{"id":337553892,"uuid":"1147422440","full_name":"floor/vlist","owner":"floor","description":"Accessible, batteries-included ultra efficient virtual list for every framework. Zero deps, 8 KB.","archived":false,"fork":false,"pushed_at":"2026-06-05T14:39:14.000Z","size":3846,"stargazers_count":10,"open_issues_count":7,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-05T16:11:53.025Z","etag":null,"topics":["lightweight","performance","typescript","virtual-list","virtual-scroll","virtualization","zero-dependencies"],"latest_commit_sha":null,"homepage":"https://vlist.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/floor.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-01T18:09:39.000Z","updated_at":"2026-06-05T14:38:45.000Z","dependencies_parsed_at":"2026-05-21T12:04:33.535Z","dependency_job_id":null,"html_url":"https://github.com/floor/vlist","commit_stats":null,"previous_names":["floor/vlist"],"tags_count":68,"template":false,"template_full_name":null,"purl":"pkg:github/floor/vlist","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/floor%2Fvlist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/floor%2Fvlist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/floor%2Fvlist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/floor%2Fvlist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/floor","download_url":"https://codeload.github.com/floor/vlist/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/floor%2Fvlist/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34035953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-07T02:00:07.652Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lightweight","performance","typescript","virtual-list","virtual-scroll","virtualization","zero-dependencies"],"created_at":"2026-05-01T02:01:23.607Z","updated_at":"2026-06-07T20:01:08.285Z","avatar_url":"https://github.com/floor.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# vlist\n\nThe virtual list library for every framework. Accessible by default, batteries-included, with composable features — in 11.0 KB.\n\n**v1.8.3** — [Changelog](./CHANGELOG.md)\n\n[![npm version](https://img.shields.io/npm/v/vlist.svg)](https://www.npmjs.com/package/vlist)\n[![CI](https://github.com/floor/vlist/actions/workflows/ci.yml/badge.svg)](https://github.com/floor/vlist/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/vlist.svg)](https://github.com/floor/vlist/blob/main/LICENSE)\n\n- **New: 11.0 KB base** — optimized from 11.2 KB, every feature bundle reduced\n- **Accessible** — WAI-ARIA, 2D keyboard navigation, focus recovery, screen-reader DOM ordering, ARIA live region\n- **Zero dependencies** — framework-agnostic core with tiny adapters for Vue, Svelte, Solid, React\n- **11.0 KB gzipped** — composable features with perfect tree-shaking\n- **Constant memory** — ~0.1 MB overhead at any scale, from 10K to 1M+ items\n- **Grid, masonry, table, groups, async, selection, sortable, scale** — all opt-in\n- **Vertical \u0026 horizontal** — single axis-neutral code path, every feature works in both orientations\n\n**18 interactive examples, docs \u0026 benchmarks → [vlist.io](https://vlist.io)**\n\n## Why vlist\n\n| | vlist | TanStack Virtual | react-virtuoso | virtua | vue-virtual-scroller |\n|---|---|---|---|---|---|\n| **A11y built-in** | WAI-ARIA + 2D keyboard | None (DIY) | Partial | Minimal | None |\n| **Grid + Masonry + Table** | All | Grid only | Grid + Table | Grid only | None |\n| **Vue** | 0.6 KB adapter | Yes | — | Yes | 11.8 KB |\n| **Svelte** | 0.5 KB adapter | Yes | — | Yes | — |\n| **Solid** | 0.5 KB adapter | Yes | — | Yes | — |\n| **Vanilla JS** | Native | Yes | — | — | — |\n| **Constant memory** | ~0.1 MB at 1M | No | No | No | No |\n\n## Framework Adapters\n\n| Framework | Package | Size |\n|-----------|---------|------|\n| Vanilla JS | `vlist` | Native — no adapter needed |\n| Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |\n| Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |\n| SolidJS | [`vlist-solidjs`](https://github.com/floor/vlist-solidjs) | 0.5 KB gzip |\n| React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |\n\n```bash\nnpm install vlist              # vanilla JS\nnpm install vlist vlist-vue    # or vlist-svelte / vlist-solidjs / vlist-react\n```\n\n## Quick Start\n\n```typescript\nimport { vlist } from 'vlist'\nimport 'vlist/styles'\n\nconst list = vlist({\n  container: '#my-list',\n  items: [\n    { id: 1, name: 'Alice' },\n    { id: 2, name: 'Bob' },\n    { id: 3, name: 'Charlie' },\n  ],\n  item: {\n    height: 48,\n    template: (item) =\u003e `\u003cdiv\u003e${item.name}\u003c/div\u003e`,\n  },\n}).build()\n\nlist.scrollToIndex(10)\nlist.setItems(newItems)\nlist.on('item:click', ({ item }) =\u003e console.log(item))\n```\n\n## Builder Pattern\n\nStart with the base, add only what you need:\n\n```typescript\nimport { vlist, withGrid, withGroups, withSelection } from 'vlist'\n\nconst list = vlist({\n  container: '#app',\n  items: photos,\n  item: { height: 200, template: renderPhoto },\n})\n  .use(withGrid({ columns: 4, gap: 16 }))\n  .use(withGroups({\n    getGroupForIndex: (i) =\u003e photos[i].category,\n    header: { height: 40, template: (cat) =\u003e `\u003ch2\u003e${cat}\u003c/h2\u003e` },\n  }))\n  .use(withSelection({ mode: 'multiple' }))\n  .build()\n```\n\n### Features\n\n| Feature | Size | Description |\n|---------|------|-------------|\n| **Base** | 11.0 KB | Virtualization, ARIA, keyboard nav, gap, padding |\n| `withAsync()` | +4.5 KB | Lazy loading with velocity-aware fetching |\n| `withSelection()` | +3.0 KB | Single/multiple selection with 2D keyboard nav |\n| `withScale()` | +3.6 KB | 1M+ items via scroll compression |\n| `withGroups()` | +4.5 KB | Sticky/inline headers with async group discovery |\n| `withAutoSize()` | +0.9 KB | Auto-measure items via ResizeObserver |\n| `withScrollbar()` | +1.9 KB | Custom scrollbar UI |\n| `withGrid()` | +4.1 KB | 2D grid layout |\n| `withMasonry()` | +3.5 KB | Pinterest-style masonry with lane-aware keyboard nav |\n| `withTable()` | +5.8 KB | Data table with columns, resize, sort |\n| `withPage()` | +0.7 KB | Window-level scrolling |\n| `withSortable()` | +2.9 KB | Drag-and-drop reordering with auto-scroll |\n| `withSnapshots()` | +1.2 KB | Scroll position save/restore |\n\n## Examples\n\nMore examples at **[vlist.io](https://vlist.io)**.\n\n### Data Table\n\n```typescript\nimport { vlist, withTable, withSelection } from 'vlist'\n\nconst table = vlist({\n  container: '#my-table',\n  items: contacts,\n  item: { height: 36, template: () =\u003e '' },\n})\n  .use(withTable({\n    columns: [\n      { key: 'name',   label: 'Name',   width: 200, sortable: true },\n      { key: 'email',  label: 'Email',  width: 260, sortable: true },\n      { key: 'role',   label: 'Role',   width: 160, sortable: true },\n      { key: 'status', label: 'Status', width: 100, align: 'center' },\n    ],\n    rowHeight: 36,\n    headerHeight: 36,\n    resizable: true,\n  }))\n  .use(withSelection({ mode: 'single' }))\n  .build()\n\ntable.on('column:sort', ({ key, direction }) =\u003e { /* re-sort data */ })\ntable.on('column:resize', ({ key, width }) =\u003e { /* persist widths */ })\n```\n\n### Grid Layout\n\n```typescript\nimport { vlist, withGrid, withScrollbar } from 'vlist'\n\nconst gallery = vlist({\n  container: '#gallery',\n  items: photos,\n  item: {\n    height: 200,\n    template: (photo) =\u003e `\n      \u003cdiv class=\"card\"\u003e\n        \u003cimg src=\"${photo.url}\" /\u003e\n        \u003cspan\u003e${photo.title}\u003c/span\u003e\n      \u003c/div\u003e\n    `,\n  },\n})\n  .use(withGrid({ columns: 4, gap: 16 }))\n  .use(withScrollbar({ autoHide: true }))\n  .build()\n```\n\n### Async Loading\n\n```typescript\nimport { vlist, withAsync } from 'vlist'\n\nconst list = vlist({\n  container: '#list',\n  item: {\n    height: 64,\n    template: (item) =\u003e item\n      ? `\u003cdiv\u003e${item.name}\u003c/div\u003e`\n      : `\u003cdiv class=\"placeholder\"\u003eLoading…\u003c/div\u003e`,\n  },\n})\n  .use(withAsync({\n    adapter: {\n      read: async ({ offset, limit }) =\u003e {\n        const res = await fetch(`/api/users?offset=${offset}\u0026limit=${limit}`)\n        const data = await res.json()\n        return { items: data.items, total: data.total, hasMore: data.hasMore }\n      },\n    },\n  }))\n  .build()\n```\n\n## Accessibility\n\nEvery vlist is accessible by default following the [WAI-ARIA listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):\n\n- **Arrow keys** move focus between items with a visible focus ring\n- **2D navigation** in grids and masonry — Up/Down by row, Left/Right by cell\n- **Masonry lane-aware nav** — arrows stay in the same visual column\n- **Home/End, PageUp/PageDown, Ctrl+Home/End** — full keyboard coverage\n- **Screen-reader DOM ordering** — items reordered on scroll idle for correct reading order\n- **ARIA live region** — announces loading state changes\n- **Focus recovery** — maintains focus when items are removed\n\nSet `interactive: false` for display-only lists (log viewers, activity feeds) where items contain their own interactive elements.\n\n## API\n\n```typescript\nconst list = vlist(config).use(...features).build()\n```\n\n### Data\n\n| Method | Description |\n|--------|-------------|\n| `list.setItems(items)` | Replace all items |\n| `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |\n| `list.prependItems(items)` | Add to start (preserves scroll position) |\n| `list.updateItem(index, partial)` | Update a single item by index |\n| `list.removeItem(index)` | Remove by index |\n| `list.getItemAt(index)` | Get item at index |\n| `list.getIndexById(id)` | Get index by item ID |\n| `list.reload()` | Re-fetch from adapter (async) |\n\n### Navigation\n\n| Method | Description |\n|--------|-------------|\n| `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \\| `'center'` \\| `'end'`) |\n| `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |\n| `list.cancelScroll()` | Cancel smooth scroll animation |\n| `list.getScrollPosition()` | Current scroll offset |\n\n### Selection (with `withSelection()`)\n\n| Method | Description |\n|--------|-------------|\n| `list.select(...ids)` | Select item(s) |\n| `list.deselect(...ids)` | Deselect item(s) |\n| `list.toggleSelect(id)` | Toggle |\n| `list.selectAll()` / `list.clearSelection()` | Bulk operations |\n| `list.getSelected()` | Array of selected IDs |\n| `list.getSelectedItems()` | Array of selected items |\n\n### Events\n\n`list.on()` returns an unsubscribe function. You can also use `list.off(event, handler)`.\n\n```typescript\nlist.on('scroll', ({ scrollPosition, direction }) =\u003e {})\nlist.on('range:change', ({ range }) =\u003e {})\nlist.on('item:click', ({ item, index, event }) =\u003e {})\nlist.on('item:dblclick', ({ item, index, event }) =\u003e {})\nlist.on('selection:change', ({ selectedIds, selectedItems }) =\u003e {})\nlist.on('load:start', ({ offset, limit }) =\u003e {})\nlist.on('load:end', ({ items, offset, total }) =\u003e {})\nlist.on('load:error', ({ error, offset, limit }) =\u003e {})\nlist.on('sort:end', ({ fromIndex, toIndex }) =\u003e {})\nlist.on('sort:cancel', ({ originalItems }) =\u003e {})\n```\n\n### Properties\n\n| Property | Description |\n|----------|-------------|\n| `list.element` | Root DOM element |\n| `list.items` | Current items (readonly) |\n| `list.total` | Total item count |\n| `list.destroy()` | Cleanup and remove from DOM |\n\n## Feature Configuration\n\nEach feature's config is fully typed — hover in your IDE for details.\n\n```typescript\nwithGrid({ columns: 4, gap: 16 })\nwithMasonry({ columns: 4, gap: 16 })\nwithGroups({ getGroupForIndex, header: { height, template }, sticky?: true })\nwithSelection({ mode: 'single' | 'multiple', initial?: [...ids] })\nwithAsync({ adapter: { read }, loading?: { cancelThreshold? } })\nwithTable({ columns, rowHeight, headerHeight?, resizable? })\nwithAutoSize()                        // auto-measure items (requires estimatedHeight)\nwithScale()                           // auto-activates at 16.7M px\nwithScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })\nwithSortable({ handle?: '.drag-handle' })  // drag-and-drop reordering\nwithPage()                            // no config — uses document scroll\nwithSnapshots({ autoSave: 'key' })    // automatic sessionStorage save/restore\n```\n\nFull configuration reference → **[vlist.io](https://vlist.io)**\n\n## Base Configuration\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `overscan` | `3` | Extra items rendered outside viewport |\n| `ariaLabel` | — | Accessible label for the listbox |\n| `orientation` | `'vertical'` | `'vertical'` or `'horizontal'` scroll direction |\n| `padding` | `0` | Content inset — number, `[v, h]`, or `[top, right, bottom, left]` |\n| `interactive` | `true` | Enable built-in keyboard navigation |\n| `reverse` | `false` | Reverse mode for chat UIs |\n| `scroll.wrap` | `false` | Wrap focus around at boundaries |\n\n## Styling\n\n```typescript\nimport 'vlist/styles'           // core (always required)\nimport 'vlist/styles/grid'      // when using withGrid()\nimport 'vlist/styles/masonry'   // when using withMasonry()\nimport 'vlist/styles/table'     // when using withTable()\nimport 'vlist/styles/extras'    // optional (variants, loading states, animations)\n```\n\nDark 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](https://vlist.io/tutorials/styling) for the full guide.\n\n## Performance\n\n| Dataset Size | After Render | Scroll Delta |\n|--------------|-------------|--------------|\n| 10K items | 0.07 MB | ~0 MB |\n| 100K items | 0.08 MB | ~0 MB |\n| 1M items | 0.09 MB | 0.19 MB |\n\n- **Initial render:** ~8ms (constant, regardless of item count)\n- **Scroll:** 120 FPS at any scale\n- **DOM nodes:** ~26 in document with 100K items (visible + overscan only)\n\nLive benchmarks against 9 competitors → **[vlist.io/benchmarks](https://vlist.io/benchmarks)**\n\n## TypeScript\n\nFully typed. Generic over your item type:\n\n```typescript\nimport { vlist, withGrid, type VList } from 'vlist'\n\ninterface Photo { id: number; url: string; title: string }\n\nconst list: VList\u003cPhoto\u003e = vlist\u003cPhoto\u003e({\n  container: '#gallery',\n  items: photos,\n  item: {\n    height: 200,\n    template: (photo) =\u003e `\u003cimg src=\"${photo.url}\" /\u003e`,\n  },\n})\n  .use(withGrid({ columns: 4 }))\n  .build()\n```\n\n## Contributing\n\n1. Fork → branch → make changes → add tests → pull request\n2. Run `bun test` and `bun run build` before submitting\n\n## License\n\n[MIT](LICENSE)\n\n## Links\n\n- **Docs \u0026 Examples:** [vlist.io](https://vlist.io)\n- **Staging:** [staging.vlist.io](https://staging.vlist.io)\n- **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)\n- **NPM:** [vlist](https://www.npmjs.com/package/vlist)\n- **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)\n\n---\n\nBuilt by [FloorIO](https://floor.io)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffloor%2Fvlist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffloor%2Fvlist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffloor%2Fvlist/lists"}