Keyboard & Animated
These integrations target the React Native renderer. For DOM lists from @legendapp/list/react, use standard DOM animation libraries or CSS transitions. For React Native Web apps using @legendapp/list/react-native, the Reanimated entrypoint works with React Native Web.
Reanimated
The Reanimated version of AnimatedLegendList supports animated props with Reanimated. Note that using Animated.createAnimatedComponent will not work as it needs more boilerplate, so you should use this instead.
Under the hood, this integration uses Reanimated.ScrollView.
Reanimated 4 sticky headers
In Reanimated 4, sticky headers can have performance problems. See Flickering/jittering while scrolling.
import { useEffect } from "react";
import { AnimatedLegendList } from "@legendapp/list/reanimated";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
export function ReanimatedExample() {
const scale = useSharedValue(0.8);
useEffect(() => {
scale.value = withSpring(1);
}, []);
return (
<AnimatedLegendList
data={data}
renderItem={renderItem}
style={useAnimatedStyle(() => ({
transform: [{ scale: scale.value }]
}))}
/>
);
}itemLayoutAnimation
Use itemLayoutAnimation to apply a Reanimated layout transition to list item containers.
import { AnimatedLegendList } from "@legendapp/list/reanimated";
import { LinearTransition } from "react-native-reanimated";
export function ReanimatedLayoutTransitionExample() {
return (
<AnimatedLegendList
data={data}
itemLayoutAnimation={LinearTransition.duration(280)}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
);
}sharedValues
Use sharedValues when you want AnimatedLegendList from @legendapp/list/reanimated to keep external Reanimated shared values in sync with list state.
This is useful when a worklet, animated style, gesture, sticky overlay, or keyboard-driven UI needs to read list state without going through React state or triggering JS rerenders. You create the shared values, pass them to the list, and LegendList updates them as scroll/list state changes.
import type { SharedValue } from "react-native-reanimated";
interface AnimatedLegendListSharedValues {
activeStickyIndex?: SharedValue<number>;
isAtEnd?: SharedValue<boolean>;
isAtStart?: SharedValue<boolean>;
isNearEnd?: SharedValue<boolean>;
isNearStart?: SharedValue<boolean>;
isWithinMaintainScrollAtEndThreshold?: SharedValue<boolean>;
scrollOffset?: SharedValue<number>;
}import { useSharedValue } from "react-native-reanimated";
import { AnimatedLegendList } from "@legendapp/list/reanimated";
export function ReanimatedSharedValuesExample() {
const scrollOffset = useSharedValue(0);
const isAtEnd = useSharedValue(false);
const isNearEnd = useSharedValue(false);
return (
<AnimatedLegendList
data={data}
keyExtractor={(item) => item.id}
renderItem={renderItem}
sharedValues={{
scrollOffset,
isAtEnd,
isNearEnd,
}}
/>
);
}Notes:
sharedValuesis only supported byAnimatedLegendListfrom@legendapp/list/reanimated.- It is not part of
@legendapp/list/animated, which is the React NativeAnimated.createAnimatedComponentwrapper. - LegendList owns writes to the shared values you pass. Treat them as list-state outputs and read them from worklets, animated styles, or gesture handlers.
scrollOffsetis the list's current scroll offset on the scroll axis. For horizontal lists it tracks the horizontal offset.- Boolean edge values use the same thresholds as the corresponding
getState()fields. - If you need JS callbacks instead of shared values, use
ref.current?.getState().listen(...).
Animated
AnimatedLegendList supports animated props with React Native's Animated.
import { useEffect, useRef } from "react";
import { Animated } from "react-native";
import { AnimatedLegendList } from "@legendapp/list/animated";
export function AnimatedExample() {
const animated = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(animated, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}).start();
}, []);
return (
<AnimatedLegendList
data={data}
renderItem={renderItem}
style={{ opacity: animated }}
/>
);
}Note that this is just a wrapper around the normal createAnimatedComponent so you can use that if you prefer.
const AnimatedLegendList = Animated.createAnimatedComponent(LegendList);KeyboardAwareLegendList
Use KeyboardAwareLegendList from @legendapp/list/keyboard for keyboard-aware scrolling, keyboard-driven insets, floating composers, and chat-style end anchoring. This was inspired by the v0 mobile app's composer and keyboard behavior.
import {
KeyboardAwareLegendList,
useKeyboardChatComposerInset,
useKeyboardScrollToEnd,
} from "@legendapp/list/keyboard";This integration depends on react-native-reanimated and react-native-keyboard-controller.
KeyboardAwareLegendList requires react-native-keyboard-controller version 1.21.7 or newer.
npm install react-native-keyboard-controller@^1.21.7 react-native-reanimatedKeyboardAwareLegendList wraps AnimatedLegendList from @legendapp/list/reanimated and uses KeyboardChatScrollView from react-native-keyboard-controller as the scroll component.
<KeyboardAwareLegendList
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ChatMessage item={item} />}
anchoredEndSpace={{ anchorIndex: messages.length - 1, anchorOffset: 16 }}
keyboardOffset={insets.bottom}
/>Useful props:
contentInsetEndAdjustment: Reanimated shared value that reserves extra end inset for a floating composer.anchoredEndSpace: reserves blank space after an anchored row, useful when sending a message that should scroll near the top of the visible area.keyboardOffset: offset passed through to the underlying keyboard scroll view, usually your bottom safe-area inset.freeze: optional Reanimated shared value used by keyboard-controller to pause keyboard scroll reactions during an imperative scroll.
Integration guidance
Do not wrap KeyboardAwareLegendList inside another KeyboardAvoidingView.
Let the list manage keyboard-aware behavior, and adjacent UI (like composers/inputs) should handle their own keyboard avoiding, for example with KeyboardStickyView.
useKeyboardChatComposerInset
Use useKeyboardChatComposerInset when a composer is outside normal list content flow, such as a KeyboardStickyView or floating input bar.
function useKeyboardChatComposerInset(
listRef: { current: Pick<LegendListRef, "reportContentInset"> | null },
composerRef: { current: Pick<View, "measure"> | null },
initialHeight?: number
): {
contentInsetEndAdjustment: SharedValue<number>;
onComposerLayout: (event: LayoutChangeEvent) => void;
};The hook:
- creates a Reanimated shared value initialized to
initialHeight(default0) - measures
composerRefonce on mount - returns
onComposerLayout, which updates the measured height when the composer layout changes - writes the measured height to
contentInsetEndAdjustment - reports
{ bottom: height }to the list withlistRef.current?.reportContentInset(...)
Pass contentInsetEndAdjustment to KeyboardAwareLegendList, then attach ref and onLayout to the composer container.
const listRef = useRef<LegendListRef>(null);
const composerRef = useRef<View>(null);
const { contentInsetEndAdjustment, onComposerLayout } =
useKeyboardChatComposerInset(listRef, composerRef, 80);
<KeyboardAwareLegendList
ref={listRef}
contentInsetEndAdjustment={contentInsetEndAdjustment}
{...props}
/>
<KeyboardStickyView>
<View ref={composerRef} onLayout={onComposerLayout}>
<Composer />
</View>
</KeyboardStickyView>useKeyboardScrollToEnd
Use useKeyboardScrollToEnd when sending a message should dismiss the keyboard and scroll the list to the end as one coordinated action.
function useKeyboardScrollToEnd(options: {
listRef: { current: { scrollToEnd(params?: { animated?: boolean }): Promise<void> } | null };
freeze?: SharedValue<boolean>;
}): {
freeze: SharedValue<boolean>;
scrollMessageToEnd: (options: { animated: boolean; closeKeyboard: boolean }) => Promise<void>;
};The hook:
- returns a
freezeshared value, or reuses the one you pass in - sets
freezetotruewhile keyboard dismissal andscrollToEndare running - calls
KeyboardController.dismiss()whencloseKeyboardis true - awaits the list's async
scrollToEnd({ animated }) - sets
freezeback tofalseafter both operations finish
Pass the returned freeze to KeyboardAwareLegendList when using scrollMessageToEnd.
const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({ listRef });
<KeyboardAwareLegendList
ref={listRef}
freeze={freeze}
{...props}
/>
requestAnimationFrame(() => {
scrollMessageToEnd({ animated: true, closeKeyboard: true });
});Legacy keyboard avoiding
@legendapp/list/keyboard-legacy exports KeyboardAvoidingLegendList for apps that still need the previous keyboard-avoiding integration, which may work better on old architecture.
import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard-legacy";It wraps AnimatedLegendList from @legendapp/list/reanimated, integrates with useKeyboardHandler from react-native-keyboard-controller, and accepts the same list props as AnimatedLegendList plus safeAreaInsetBottom.
Use KeyboardAwareLegendList from @legendapp/list/keyboard for new chat and floating-composer screens.
Chat Example
For chat screens, KeyboardAwareLegendList works with a few chat-specific pieces:
KeyboardStickyViewkeeps the composer attached to the keyboard while the list fills the remaining space.useKeyboardChatComposerInsetmeasures the composer and keeps the list's end inset in sync.useKeyboardScrollToEndcoordinates the imperative scroll with keyboard dismissal after a message is sent.anchoredEndSpacereserves space after the newly sent message so it can land near the top of the visible area instead of being hidden behind the composer.initialScrollAtEndstarts the conversation at the latest message, whilemaintainVisibleContentPositionkeeps the viewport stable as new rows arrive.
import { useRef, useState } from "react";
import { Button, TextInput, View } from "react-native";
import { KeyboardGestureArea, KeyboardProvider, KeyboardStickyView } from "react-native-keyboard-controller";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
KeyboardAwareLegendList,
useKeyboardChatComposerInset,
useKeyboardScrollToEnd,
} from "@legendapp/list/keyboard";
import type { LegendListRef } from "@legendapp/list/react-native";
export function KeyboardChatExample() {
const listRef = useRef<LegendListRef>(null);
const composerRef = useRef<View>(null);
const [messages, setMessages] = useState(defaultChatMessages);
const [anchorIndex, setAnchorIndex] = useState<number | undefined>(undefined);
const [inputText, setInputText] = useState("");
const insets = useSafeAreaInsets();
const { contentInsetEndAdjustment, onComposerLayout } =
useKeyboardChatComposerInset(listRef, composerRef, 80);
const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({ listRef });
const sendMessage = () => {
const text = inputText || "Empty message";
if (text.trim()) {
// Anchor the list at the message being sent so it can scroll above the composer.
setAnchorIndex(messages.length);
setMessages((messagesNew) => [
...messagesNew,
{ id: String(idCounter++), sender: "user", text, timeStamp: Date.now() },
]);
setInputText("");
// Wait for React to commit the new row before measuring and scrolling to the end.
requestAnimationFrame(() => {
scrollMessageToEnd({ animated: true, closeKeyboard: true });
});
}
};
return (
<KeyboardProvider>
<View style={[styles.container, { paddingBottom: insets.bottom, paddingTop: insets.top }]}>
<KeyboardGestureArea interpolator="ios" offset={60} style={styles.container}>
<KeyboardAwareLegendList
alignItemsAtEnd
anchoredEndSpace={anchorIndex !== undefined ? { anchorIndex } : undefined}
contentContainerStyle={styles.contentContainer}
contentInsetEndAdjustment={contentInsetEndAdjustment}
data={messages}
estimatedItemSize={80}
freeze={freeze}
initialScrollAtEnd
keyExtractor={(item) => item.id}
keyboardDismissMode="interactive"
keyboardOffset={insets.bottom}
maintainScrollAtEnd
maintainVisibleContentPosition
ref={listRef}
renderItem={({ item }) => <ChatMessage item={item} />}
style={styles.list}
/>
</KeyboardGestureArea>
<KeyboardStickyView offset={{ closed: 0, opened: insets.bottom }}>
<View ref={composerRef} onLayout={onComposerLayout} style={styles.inputContainer}>
<TextInput
onChangeText={setInputText}
placeholder="Type a message"
style={styles.input}
value={inputText}
/>
<Button onPress={sendMessage} title="Send" />
</View>
</KeyboardStickyView>
</View>
</KeyboardProvider>
);
}