Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/app/docs/docsNavigationSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export const docsNavigationSections = [
{
title:"Performance",
path:"/docs/guides/performance"
},
{
title:"Large Lists & Virtualization",
path:"/docs/guides/large-lists-and-virtualization"
}
]
},
Expand Down
70 changes: 70 additions & 0 deletions docs/app/docs/guides/large-lists-and-virtualization/content.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Large lists and virtualization

Guidance for using Rad UI in performance-sensitive collections such as long menus, select options, combobox results, and trees.

## When to virtualize

Consider virtualization when **any** of these are true:

- rendering more than ~100 interactive rows measurably slows typing or pointer moves
- opening a popover causes a visible frame drop on mid-tier hardware
- keyboard navigation (arrow keys, typeahead) lags behind input

Rad UI primitives remain accessible and composable at scale, but React still pays a cost to reconcile large subtrees. Virtualization limits mounted DOM nodes to the visible window plus a small overscan buffer.

## Components that commonly hit limits

| Pattern | Hot path | Mitigation |
| --- | --- | --- |
| `Select` / `Combobox` with many options | open popover + initial focus | virtualize the list body; keep the trigger mounted |
| `Menu` / `DropdownMenu` with long action lists | arrow-key traversal | group infrequent actions; paginate or virtualize |
| `Tree` with deep/wide data | expand/collapse + roving focus | lazy-load branches; virtualize siblings at each level |
| Tables built from repeated rows | scroll + selection | virtualize rows; keep header/footer outside the window |

## Recommended integration pattern

1. **Filter before render** when possible (server search, debounced query).
2. **Virtualize the collection**, not the entire overlay — keep Rad UI trigger, portal, and focus scope intact.
3. **Preserve stable item keys** so focus and selection survive scroll.
4. **Expose item height** to your virtualizer (fixed height is simplest; dynamic height needs measurement).

```tsx
// Conceptual pattern — swap in your virtualizer (react-window, TanStack Virtual, etc.)
<Combobox.Root>
<Combobox.Trigger />
<Combobox.Content>
<VirtualList
itemCount={items.length}
itemSize={32}
renderItem={({ index, style }) => (
<Combobox.Item key={items[index].id} value={items[index].id} style={style}>
{items[index].label}
</Combobox.Item>
)}
/>
</Combobox.Content>
</Combobox.Root>
```

Adapt to the exact anatomy of the component you use; the important part is that **only visible items mount** inside the scrollable region.

## Keyboard and accessibility

When virtualizing:

- ensure the active item stays mounted while focused, or scroll it into view before moving focus
- keep typeahead targeting visible rows or maintain a separate search index
- do not remove `role` / `aria-*` attributes from items that are mounted
- test with screen readers after scroll — position in set should remain meaningful

Rad UI handles roving tabindex inside the mounted set. Your virtualizer must scroll items into view before focus moves to off-screen indices.

## What not to do

- mounting thousands of `Menu.Item` nodes without measurement
- re-creating the entire items array on every keystroke without memoization
- virtualizing at the page root while leaving multiple open overlays mounted

## Related guidance

See [Performance and memoization](/docs/guides/performance) before optimizing, and test focus restoration when combining virtual scroll containers with portaled overlays.
7 changes: 7 additions & 0 deletions docs/app/docs/guides/large-lists-and-virtualization/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createDocsPage } from '@/components/docsPage/createDocsPage'
import metadata from './seo'
import Content from './content.mdx'

export { metadata }

export default createDocsPage(Content)
8 changes: 8 additions & 0 deletions docs/app/docs/guides/large-lists-and-virtualization/seo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import generateSeoMetadata from '@/utils/seo/generateSeoMetadata'

const virtualizationGuideMetadata = generateSeoMetadata({
title: 'Large lists and virtualization | Rad UI',
description: 'When and how to virtualize long Select, Menu, Combobox, and Tree collections with Rad UI.'
})

export default virtualizationGuideMetadata
Loading