# Keyboard & Animated

Keyboard & Animated — Legend List documentation.

Source: https://legend.so/list/v3/react-native/keyboard-and-animated/

Legend List v3 documentation. Current documentation version.

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](https://legend.so/list/v3/react-native/keyboard-and-animated/#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](https://docs.swmansion.com/react-native-reanimated/docs/guides/performance/#%EF%B8%8F-flickeringjittering-while-scrolling).

```jsx
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](https://legend.so/list/v3/react-native/keyboard-and-animated/#itemlayoutanimation)

Use `itemLayoutAnimation` to apply a Reanimated layout transition to list item containers.

```tsx
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](https://legend.so/list/v3/react-native/keyboard-and-animated/#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.

```ts
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>;
}
```

```tsx
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:

-   `sharedValues` is only supported by `AnimatedLegendList` from `@legendapp/list/reanimated`.
-   It is not part of `@legendapp/list/animated`, which is the React Native `Animated.createAnimatedComponent` wrapper.
-   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.
-   `scrollOffset` is 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](https://legend.so/list/v3/react-native/keyboard-and-animated/#animated)

AnimatedLegendList supports animated props with React Native's Animated.

```jsx
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.

```ts
const AnimatedLegendList = Animated.createAnimatedComponent(LegendList);
```

## [KeyboardAwareLegendList](https://legend.so/list/v3/react-native/keyboard-and-animated/#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](https://v0.app/ios) composer and keyboard behavior.

```ts
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.

```bash
npm install react-native-keyboard-controller@^1.21.7 react-native-reanimated
```

`KeyboardAwareLegendList` wraps `AnimatedLegendList` from `@legendapp/list/reanimated` and uses `KeyboardChatScrollView` from `react-native-keyboard-controller` as the scroll component.

```tsx
<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](https://legend.so/list/v3/react-native/keyboard-and-animated/#usekeyboardchatcomposerinset)

Use `useKeyboardChatComposerInset` when a composer is outside normal list content flow, such as a `KeyboardStickyView` or floating input bar.

```ts
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` (default `0`)
-   measures `composerRef` once on mount
-   returns `onComposerLayout`, which updates the measured height when the composer layout changes
-   writes the measured height to `contentInsetEndAdjustment`

`KeyboardAwareLegendList` reports the combined keyboard and composer inset through its scroll component.

Pass `contentInsetEndAdjustment` to `KeyboardAwareLegendList`, then attach `ref` and `onLayout` to the composer container.

```tsx
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](https://legend.so/list/v3/react-native/keyboard-and-animated/#usekeyboardscrolltoend)

Use `useKeyboardScrollToEnd` when sending a message should dismiss the keyboard and scroll the list to the end as one coordinated action.

```ts
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 `freeze` shared value, or reuses the one you pass in
-   sets `freeze` to `true` while keyboard dismissal and `scrollToEnd` are running
-   calls `KeyboardController.dismiss()` when `closeKeyboard` is true
-   awaits the list's async `scrollToEnd({ animated })`
-   sets `freeze` back to `false` after both operations finish

**New in 3.7:** `scrollMessageToEnd({ animated: true, closeKeyboard: true })` starts scrolling immediately and eases to the closed-keyboard end in one smooth motion while the keyboard closes. Keyboard dismissal is shared by overlapping sends, so it happens once during that close.

Pass the returned `freeze` to `KeyboardAwareLegendList` when using `scrollMessageToEnd`.

```tsx
const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({ listRef });

<KeyboardAwareLegendList
  ref={listRef}
  freeze={freeze}
  {...props}
/>

requestAnimationFrame(() => {
  scrollMessageToEnd({ animated: true, closeKeyboard: true });
});
```

### [Legacy keyboard avoiding](https://legend.so/list/v3/react-native/keyboard-and-animated/#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.

```ts
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](https://legend.so/list/v3/react-native/keyboard-and-animated/#chat-example)

For chat screens, `KeyboardAwareLegendList` works with a few chat-specific pieces:

-   `KeyboardStickyView` keeps the composer attached to the keyboard while the list fills the remaining space.
-   `useKeyboardChatComposerInset` measures the composer and keeps the list's end inset in sync.
-   `useKeyboardScrollToEnd` coordinates the imperative scroll with keyboard dismissal after a message is sent.
-   `anchoredEndSpace` reserves space after the newly sent message so it can land near the top of the visible area instead of being hidden behind the composer.
-   `initialScrollAtEnd` starts the conversation at the latest message, while `maintainVisibleContentPosition` keeps the viewport stable as new rows arrive.

```tsx
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>
  );
}
```

[View Markdown](https://legend.so/list/v3/react-native/keyboard-and-animated.md)[Report a documentation correction](https://github.com/LegendApp/legend-docs/issues/new?title=Docs%3A+Keyboard+%26+Animated&body=Page%3A+https%3A%2F%2Flegend.so%2Flist%2Fv3%2Freact-native%2Fkeyboard-and-animated%0A%0AWhat+is+incorrect+or+missing%3F%0A%0AExpected+behavior+or+supporting+source%3A%0A)
