Skip to main content

List

Render caller-defined rows with optional virtual scrolling.

Use List when the caller owns each row’s markup and needs a stable collection surface. Pass items and an itemRender component; List does not take row content through children. Add a virtual renderer for large collections only when ordinary rendering becomes costly.

Basic usage#

import { List } from 'moraine'

export function Example() {
  return <List items={['Alpha', 'Beta']} itemRender={(item) => <li>{item.item}</li>} />
}

Playground#

  • Alpha
  • Beta
Props
Slots

Anatomy#

List [component; slot=root; <ul>]

Usage#

Row ownership and virtualization#

itemRender receives the item, its index, and row props. Apply those props to the final row element when using virtualRender; the virtualizer needs them for positioning and measurement. The default root is a <ul> with role="list", so render list-item semantics in each row or choose a different root with as for another structure. TypeScript infers the item and root element types from items and as.

createListVirtualizer is an optional adapter from moraine/virtualizer and needs @tanstack/virtual-core. The visible row count and scroll behavior depend on your estimated or measured row sizes.

Read the Virtualization guide for setup, row measurements, stable keys, and keyboard navigation.

import { List } from 'moraine'
import { createListVirtualizer } from 'moraine/virtualizer'

export function Virtualization() {
  const ITEMS = Array.from({ length: 10_000 }, (_, index) => ({
    id: index + 1,
    label: `Result ${index + 1}`,
  }))

  type Item = (typeof ITEMS)[number]

  const virtualizer = createListVirtualizer<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>
      )}
    />
  )
}

Examples#

Dynamic heights#

import { List } from 'moraine'
import { createListVirtualizer } from 'moraine/virtualizer'
import { For } from 'solid-js'

export 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 = createListVirtualizer<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>
      )}
    />
  )
}

Props#

Renders a <ul> element by default.

Prop