icon

Morainev0.5.0

List
elementslist

List

Headless collection renderer with polymorphic roots and caller-controlled virtualization.

Import#

import { List } from 'moraine'
import { useListVirtualizer } from 'moraine/utils'

List owns collection traversal and the scroll-element bridge used by virtual renderers. Item markup, semantics, styling, filtering, focus, and selection remain under caller control.

Examples#

Object List#

Render arbitrary objects with a semantic ul root, role="list", and data-slot="root" by default. Use as and native root attributes when another element or role is required; explicit attributes override the defaults.

function Basic() {
return (
<List
items={JOBS}
aria-label="Open positions"
class="border border-border rounded-md divide-border divide-y"
itemRender={(context) => (
<li class="p-3 flex gap-3 items-center justify-between">
<span class="flex flex-col min-w-0">
<span class="font-medium">{context.item.title}</span>
<span class="text-sm text-muted-foreground">{context.item.team}</span>
</span>
<span class="text-xs text-muted-foreground shrink-0">{context.item.location}</span>
</li>
)}
/>
)
}

Virtualization#

Import useListVirtualizer from moraine/utils. It provides a ready-to-use virtualRender, automatically connects the scroll element, positions rows, and forwards dynamic measurement refs. Configure estimateSize, a stable getItemKey, and optional TanStack Virtual settings such as gap and overscan.

The built-in adapter requires @tanstack/virtual-core as an optional peer dependency. Install a compatible version in your application before using it.

Terminal window
bun add @tanstack/virtual-core

Provide a custom virtualRender component when the built-in absolute row layout is not suitable.

function Virtualization() {
const ITEMS = Array.from({ length: 10_000 }, (_, index) => ({
id: index + 1,
label: `Result ${index + 1}`,
}))
type Item = (typeof ITEMS)[number]
const virtualizer = useListVirtualizer<Item, HTMLElement, HTMLDivElement>({
estimateSize: () => 36,
getItemKey: (item) => item.id,
overscan: 8,
})
return (
<List
as="div"
items={ITEMS}
virtualRender={virtualizer.virtualRender}
role="list"
aria-label="Virtual results"
class="border border-border rounded-md h-72 w-full overflow-y-auto"
itemRender={(context) => (
<div {...context.props} role="listitem">
<div class="px-3 border-b border-border flex h-9 items-center">{context.item.label}</div>
</div>
)}
/>
)
}

Dynamic Heights#

For variable-height rows, provide a reasonable estimate and let the built-in renderer measure connected rows. TanStack Virtual observes later content size changes and recalculates positions; gap remains consistent between measured rows.

function DynamicHeight() {
const ITEMS = Array.from({ length: 1_000 }, (_, index) => ({
id: index + 1,
label: `Result ${index + 1}`,
details: Array.from(
{ length: (index % 4) + 1 },
(_, detailIndex) => `Detail line ${detailIndex + 1} for result ${index + 1}.`,
),
}))
type Item = (typeof ITEMS)[number]
const virtualizer = useListVirtualizer<Item, HTMLElement, HTMLDivElement>({
estimateSize: () => 96,
getItemKey: (item) => item.id,
gap: 8,
overscan: 8,
})
return (
<List
as="div"
items={ITEMS}
virtualRender={virtualizer.virtualRender}
role="list"
aria-label="Variable-height results"
class="py-2 border border-border rounded-md h-80 w-full overflow-y-auto"
itemRender={(context) => (
<div {...context.props} role="listitem">
<div class="mx-2 px-3 py-2 border border-border rounded-md">
<div class="font-medium">{context.item.label}</div>
<div class="text-sm text-muted-foreground">
<For each={context.item.details}>
{(detail) => <span class="block">{detail}</span>}
</For>
</div>
</div>
</div>
)}
/>
)
}

API Reference#

Props#

PropTypeDefaultDescription
asT | undefinedul
Root element or component.
classClassValue
Class applied to the component root or trigger element.
classesListT.Classes | undefined
itemRender*ComponentOrElement<ListT.ItemRenderProps<TItem, TItemElement>>
Renders one collection item.
itemsreadonly TItem[] | undefined
Reactive collection rendered by the list.
refJSX.HTMLElementTags[T] extends { ref?: infer Ref; } ? Ref : never | undefined
styleJSX.CSSProperties | undefined
stylesListT.Styles | undefined
virtualRenderComponent<ListT.VirtualRenderProps<TItem, HTMLElement, TItemElement>> | undefined
Replaces normal iteration with caller-controlled virtual rendering.

ARIA#

Accessibility attributes and roles emitted by the component markup.
AttributeTypeDefaultDescription
rolestring
Defines the semantic role exposed to assistive technology.

Data Attributes#

State and slot attributes exposed for styling hooks and selectors.
AttributeTypeDefaultDescription
data-slotstring
Identifies the rendered slot for styling hooks and selectors.