Guides
Practical recipes for common Legend List use cases.
Chat Interfaces
Use this when your messages should start at the bottom without using inverted.
alignItemsAtEnd?: boolean;
maintainScrollAtEnd?: boolean;
maintainScrollAtEndThreshold?: number;<LegendList
data={items}
renderItem={({ item }) => <Text>{item.title}</Text>}
estimatedItemSize={320}
alignItemsAtEnd
maintainScrollAtEnd
maintainScrollAtEndThreshold={0.1}
/>Pitfalls:
- Avoid
inverted; it can cause animation and scroll edge cases. - Tune
maintainScrollAtEndThresholdfor your UX.
Initial Positioning
Use declarative initial scroll props when the first viewport should start at a specific item or at the end of the list.
initialScrollAtEnd?: boolean;
initialScrollIndex?: number | { index: number; viewOffset?: number; viewPosition?: number };
initialScrollOffset?: number;<LegendList
data={messages}
keyExtractor={(item) => item.id}
renderItem={renderMessage}
estimatedItemSize={72}
initialScrollIndex={{ index: highlightedIndex, viewPosition: 0.5 }}
/>For chat and timeline screens, prefer initialScrollAtEnd over calling scrollToEnd after mount.
<LegendList
data={messages}
keyExtractor={(item) => item.id}
renderItem={renderMessage}
estimatedItemSize={72}
initialScrollAtEnd
maintainScrollAtEnd
/>Pitfalls:
- Prefer
initialScrollIndex,initialScrollOffset, orinitialScrollAtEndfor initial placement instead of imperative scroll calls inuseEffect. - Provide
getFixedItemSizewhen target rows have exact fixed sizes. UseestimatedItemSizeonly as a rough initial offset hint when dynamic rows are significantly different from100px. - Use stable keys so measurement caches can survive data refreshes.
Floating Composer / Overlay Insets
Use this when a composer or input bar visually covers the end of the list but should stay outside the list's normal content.
contentInsetEndAdjustment?: number;
anchoredEndSpace?: {
anchorIndex: number;
anchorOffset?: number;
anchorMaxSize?: number;
onSizeChanged?: (size: number) => void;
};On web, measure the overlay and pass that height to contentInsetEndAdjustment.
<LegendList
data={messages}
renderItem={renderMessage}
keyExtractor={(item) => item.id}
initialScrollAtEnd
maintainScrollAtEnd
anchoredEndSpace={sentIndex !== undefined ? { anchorIndex: sentIndex } : undefined}
contentInsetEndAdjustment={composerHeight + 24}
/>On React Native, use the keyboard-aware integration and pass the shared value returned by useKeyboardChatComposerInset.
const listRef = useRef<LegendListRef>(null);
const composerRef = useRef<View>(null);
const { contentInsetEndAdjustment, onComposerLayout } =
useKeyboardChatComposerInset(listRef, composerRef);
<KeyboardAwareLegendList
ref={listRef}
data={messages}
renderItem={renderMessage}
keyExtractor={(item) => item.id}
initialScrollAtEnd
contentInsetEndAdjustment={contentInsetEndAdjustment}
/>
<KeyboardStickyView>
<View ref={composerRef} onLayout={onComposerLayout}>
<Composer />
</View>
</KeyboardStickyView>Pitfalls:
- Prefer
contentInsetEndAdjustmentover padding the list content when the overlay size changes dynamically. - Use
anchoredEndSpacefor the row you want to land near the start after sending. - Keep a stable
keyExtractor; changing keys while adjusting overlay inset will discard size and position caches.
Web Layout and Window Scroll
Use the React entrypoint for DOM-native lists:
import { LegendList } from "@legendapp/list/react";For contained lists, the scroll container needs a real height.
<div style={{ height: 480, minHeight: 0 }}>
<LegendList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
style={{ height: "100%" }}
/>
</div>For pages that already scroll at the document level, use useWindowScroll.
<LegendList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
useWindowScroll
/>Pitfalls:
- In flex layouts, make sure parent containers can shrink, usually with
minHeight: 0. - Use
contentContainerStylefor item spacing such asgap;gap-*classes oncontentContainerClassNamedo not control virtualized item spacing. - Use the React Native entrypoint instead if your app renders through React Native Web.
Infinite Scrolling
Use onEndReached for standard feeds and onStartReached for prepending older items.
onStartReached?: ((info: { distanceFromStart: number }) => void) | null | undefined;
onStartReachedThreshold?: number;
onEndReached?: ((info: { distanceFromEnd: number }) => void) | null | undefined;
onEndReachedThreshold?: number;<LegendList
data={data}
renderItem={({ item }) => <MessageItem item={item} />}
keyExtractor={(item) => item.id}
onEndReached={loadMoreAtEnd}
onStartReached={loadMoreAtStart}
onEndReachedThreshold={0.5}
onStartReachedThreshold={0.5}
maintainVisibleContentPosition={{ data: true }}
recycleItems
/>Pitfalls:
- Guard against duplicate loads (
loadingstate or request dedupe). - For prepend flows, keep
maintainVisibleContentPosition={{ data: true }}.
Snap to Indices
Use snapToIndices when specific rows should become scroll snap points.
<LegendList
data={sections}
estimatedItemSize={320}
getFixedItemSize={(item) => item.height}
keyExtractor={(item) => item.id}
renderItem={renderSection}
snapToIndices={[0, 4, 8, 12]}
/>On web, snap indices can point to items outside the currently mounted DOM window. LegendList computes snap offsets from list measurements and estimates, so virtualization does not require every snap target to be mounted.
Pitfalls:
- Use
getFixedItemSizefor exact snap positions when item sizes are fixed. - For dynamic rows,
estimatedItemSizeonly improves the initial snap offset before rows are measured. It is usually worth setting only when rows are far from the default100px. - Keep snap indices within the data range and update them when the data shape changes.
Always Render
Use alwaysRender to keep specific rows mounted outside the virtualized window.
<LegendList
data={data}
keyExtractor={(item) => item.id}
estimatedItemSize={48}
alwaysRender={{ top: 2, bottom: 2 }}
renderItem={({ item, index }) => (
<Row label={item.title} pinned={index < 2 || index >= data.length - 2} />
)}
/>alwaysRender accepts:
top/bottom: keep first/last N items mountedindices: keep explicit indices mountedkeys: keep specific keys mounted (requireskeyExtractor)
Maintain Visible Content Position
Use this when data or size changes above the viewport should not move what the user is reading.
maintainVisibleContentPosition?:
| boolean
| {
data?: boolean;
size?: boolean;
shouldRestorePosition?: (item: ItemT, index: number, data: ItemT[]) => boolean;
};Defaults:
size: truestabilizes scroll during size/layout changesdata: falsedoes not anchor on data changes unless enabled
Common setup for prepend-heavy feeds:
<LegendList
data={messages}
maintainVisibleContentPosition={{ data: true, size: true }}
onStartReached={loadOlderMessages}
/>Item Size Hints
LegendList measures dynamic items automatically, so size props are optional. In v3, estimatedItemSize is mostly just an initial container allocation hint before measurement. The default estimate is 100px, and after rows render LegendList uses measured sizes and averages.
Only set estimatedItemSize when most rows are significantly larger or smaller than 100px, or when a far initial scroll / snap target needs a better first offset.
<LegendList
data={items}
estimatedItemSize={88}
getItemType={(item) => item.kind}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>Use getFixedItemSize when the rendered size is exact. This is a stronger optimization than estimatedItemSize because those rows do not need measurement.
<LegendList
data={rows}
getFixedItemSize={(item) => (item.kind === "header" ? 48 : 72)}
keyExtractor={(item) => item.id}
renderItem={renderRow}
/>To inspect measured averages after rows render, use getState().getAverageItemSizes().
const averages = listRef.current?.getState().getAverageItemSizes();Pitfalls:
- Use
getItemTypewhen item families have meaningfully different average sizes. - Do not tune
estimatedItemSizejust to match measured averages closely; it mostly affects initial container count.
SectionList patterns
For grouped data with headers/footers per section, use SectionList.
import { SectionList } from "@legendapp/list/section-list";
<SectionList
sections={sections}
keyExtractor={(item) => item.id}
renderSectionHeader={({ section }) => <Header title={section.title} />}
renderItem={({ item }) => <Row item={item} />}
stickySectionHeadersEnabled
estimatedItemSize={48}
/>For full prop and method details (including scrollToLocation), see API Reference.