1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Virtual List

Virtual List

ui

A long list that only puts the rows you can see into the DOM: five thousand rows render as about thirty nodes, so the page stops taking seconds to paint and scrolling stops stuttering. Reach for it on an admin table or data grid, a log, audit or event viewer, chat and message history, a notification or activity feed, search results over a big local array, a file or asset browser, a leaderboard, a transaction or order history, a select or combobox with thousands of options, a long picker of countries, currencies or timezones, and any list where you already hold every row in memory. Common asks it answers: "virtual list react", "virtualized list react", "list virtualization", "windowing react", "react-window alternative", "react-virtualized alternative", "react-virtuoso alternative", "virtua alternative", "@tanstack/react-virtual", "TanStack Virtual without the wiring", "virtual scrolling react", "infinite list vs virtual list", "render 10000 rows react", "render 100k rows", "long list is slow to render", "list lags when scrolling", "scroll performance long list", "page freezes with large table", "only render visible items", "list virtualization with dynamic row heights", "variable height virtual list", "virtual list unknown row height", "measure row height virtual list", "virtual list scroll jumps", "scroll position jumps when prepending items", "chat keeps jumping when loading older messages", "maintain scroll position when adding items above", "virtual list loses focus", "focus lost when row unmounts", "screen reader virtualized list", "aria-posinset aria-setsize list", "accessible virtual list", "virtual list scrollToIndex", "restore scroll position react list", "shadcn virtual list", "shadcn table 1000 rows slow", "next.js virtualized list", "virtual list app router", "仮想スクロール react", "大量の行 描画 遅い", "リスト 仮想化", "スクロール位置 ずれる 追加". **shadcn/ui ships no virtualization, and that is a measurement rather than an impression:** fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,287 bytes of it, the strings virtual, virtualiz, windowing, overscan, ResizeObserver, aria-posinset, aria-setsize, scrollToIndex, getVirtualItems, estimateSize and translateY are every one of them a zero hit. Its `table` renders every row you hand it and its `scroll-area` hands scrolling to Radix without measuring anything, so this gets wired by hand against TanStack Virtual or react-window each time — and the same four things break every time. **Focus survives.** The row you tabbed into stays mounted after it scrolls out of the window, instead of being unmounted under you and dropping focus to the top of the page — the failure that makes a keyboard user lose their place in a list they were working down. **Screen readers get the real position.** Every row carries `aria-posinset` and `aria-setsize`, so it is announced as "item 4,213 of 5,000" rather than as a count of the handful that happen to be mounted, and the spacer holding the scroll height is marked presentational so the list and its items stay related. A virtualized list without this tells a screen reader a number that is simply false. **The view does not jump.** Rows are measured as they mount with a `ResizeObserver`, and when a row above the viewport turns out taller than the estimate — or older rows are prepended — the scroll offset is corrected against a row-keyed anchor in a layout effect, before the browser paints. That anchor is why prepending older chat messages keeps the message you were reading exactly where it was, which is the single most complained-about behaviour in hand-rolled versions. **And positions can be restored**, via `defaultScrollOffset` plus a ref handle with `scrollToIndex(index, "auto" | "start" | "center" | "end")`, `scrollToOffset` and `getScrollOffset` — so returning from a detail page lands where the reader left. Rows may be any height and nothing has to be declared up front; `estimateItemHeight` (default 48) is only the guess used before a row has been measured, and `overscan` (default 4) sets how many rows are kept mounted beyond the edges. Controlled by `count` plus a render function — `children` is called with an index, so the data can live anywhere — with `itemKey` for stable identity, `onScroll` and `empty`. Defaults to `role="list"` / `listitem`; pass `role="listbox"` and `itemRole="option"` when the rows are selectable. Set the height with `className` (the default is `h-72`); rows are absolutely positioned, so give them padding rather than a vertical margin. Vertical only, and find-in-page reaches mounted rows only, which is inherent to windowing rather than a shortcoming of this one — if a reader has to be able to Cmd-F the whole list, do not virtualize it. Styled with shadcn tokens so it follows light and dark themes, and it ships with **no dependencies at all** — no Radix, no virtualization library. Within pulld it is the counterpart to infinite-scroll, and the two are easy to confuse: that one fetches the next page when the reader nears the end and is about *where the data comes from*, while this one is about *how many rows exist in the DOM*, over data you already hold — a list that loads endlessly and never virtualizes gets slower with every page, and the two compose. Beside it sit sticky-table-header and sort-header for long tables, and table-row-expander for rows that open.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/virtual-list.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
components/ui/virtual-list.tsx