ChatGPT and Claude-style smart scrolling for React Native message lists.
A FlatList-compatible React Native component that replicates ChatGPT/Claude-like "new message snaps to top" scrolling behavior for conversational UIs where the last item can grow over time (e.g., streaming AI responses).
Try it live on Expo Snack: Android • iOS • Web
npm install react-native-streaming-message-list react-native-reanimated
This library requires react-native-reanimated. Follow their installation guide if you haven't already.
We provide multiple example apps showcasing different use cases:
See the examples README for how to run and switch between demos.
💡 For a complete working example, check out the examples folder.
Replace FlatList with StreamingMessageList. This component is built on @legendapp/list and accepts the same FlatList-like props:
- import { FlatList } from 'react-native';
+ import { StreamingMessageList } from 'react-native-streaming-message-list';
- <FlatList
+ <StreamingMessageList
data={messages}
keyExtractor={(item) => item.id}
renderItem={renderMessage}
/>
Create a state variable to track when messages are actively streaming:
const [isStreaming, setIsStreaming] = useState(false);
Pass it to StreamingMessageList:
<StreamingMessageList
data={messages}
keyExtractor={(item) => item.id}
renderItem={renderMessage}
+ isStreaming={isStreaming}
/>
To enable smart scrolling, wrap two special messages:
AnchorItem (this message will stay near the top)StreamingItem (tracks height changes during and after streaming)import { AnchorItem, StreamingItem } from 'react-native-streaming-message-list';
const renderMessage = ({ item, index }) => {
const isLastUserMessage =
item.role === 'user' &&
messages.findLastIndex(m => m.role === 'user') === index;
const isLastAssistantMessage =
item.role === 'assistant' &&
messages.findLastIndex(m => m.role === 'assistant') === index;
let content = <YourMessageBubble message={item} />;
if (isLastUserMessage) {
content = <AnchorItem>{content}</AnchorItem>;
} else if (isLastAssistantMessage) {
content = <StreamingItem>{content}</StreamingItem>;
}
return content;
};
That's it! The list will now handle ChatGPT-style scrolling automatically.
Need more? See the examples folder for complete, runnable chat apps with different UI styles.
StreamingMessageList: Your main list component. Use it instead of FlatList for any chat/message list where content can stream or grow.
AnchorItem: Wrap the last user message. This keeps it visible near the top while the assistant response grows below it.
StreamingItem: Wrap the last assistant message. Keep this wrapper even after streaming ends to track height changes (like action buttons appearing).
For message animations, use Animated.View from react-native-reanimated with the entering prop.
Recommended approach: Animate only the first user message with a fade-in. The "slide-in" effect for new messages happens naturally through the library's placeholder and scroll-to-bottom behavior, so additional slide animations are unnecessary:
import Animated, { FadeIn } from 'react-native-reanimated';
const renderMessage = ({ item, index }) => {
const isFirstUserMessage =
item.role === 'user' &&
messages.findIndex(m => m.role === 'user') === index;
let content = <YourMessageBubble message={item} />;
return (
<Animated.View entering={isFirstUserMessage ? FadeIn.duration(350) : undefined}>
{content}
</Animated.View>
);
};
See the Reanimated documentation for more animation options.
AnchorItemisStreaming={true} and wrap the new assistant message with StreamingItemisStreaming={false}Use useStreamingMessageList to show a button when the user scrolls away from the bottom:
import {
StreamingMessageList,
StreamingMessageListProvider,
useStreamingMessageList,
} from 'react-native-streaming-message-list';
const ScrollToBottomButton = ({ listRef }) => {
const { isAtEnd, contentFillsViewport } = useStreamingMessageList();
if (isAtEnd || !contentFillsViewport) return null;
return (
<TouchableOpacity
style={styles.scrollButton}
onPress={() => listRef.current?.scrollToEnd({ animated: true })}
>
<Text>↓</Text>
</TouchableOpacity>
);
};
const listRef = useRef(null);
<StreamingMessageListProvider>
<View style={{ flex: 1 }}>
<StreamingMessageList ref={listRef} data={messages} ... />
<ScrollToBottomButton listRef={listRef} />
</View>
</StreamingMessageListProvider>
<StreamingMessageList>Main component that wraps your message list with smart scroll behavior.
Extends all FlatList props from @legendapp/list, plus:
| Prop | Type | Required | Description |
|---|---|---|---|
data |
T[] |
Yes | Array of message items |
renderItem |
(info) => ReactNode |
Yes | Function to render each item |
keyExtractor |
(item, index) => string |
Yes | Unique key for each item |
isStreaming |
boolean |
No | Whether content is currently updating (triggers smart scroll) |
config |
StreamingMessageListConfig |
No | Advanced configuration |
type StreamingMessageListConfig = {
debounceMs?: number; // Debounce for placeholder height calculations (default: 150)
placeholderStableDelayMs?: number; // Delay before placeholder is considered stable (default: 200)
isAtEndThreshold?: number; // Threshold in pixels for isAtEnd calculation (default: 10)
};
<AnchorItem>Wrapper for the message that should be "anchored" near the top when a new conversation turn begins (typically the last user message).
<AnchorItem>
<YourMessageBubble />
</AnchorItem>
<StreamingItem>Wrapper for the last assistant message.
<StreamingItem>
<YourMessageBubble />
</StreamingItem>
<StreamingMessageListProvider>Optional provider that enables access to scroll metrics via useStreamingMessageList. Wrap your list and any components that need scroll metrics with this provider.
<StreamingMessageListProvider>
<StreamingMessageList ... />
<YourScrollButton />
</StreamingMessageListProvider>
useStreamingMessageList()Hook to access scroll metrics. Must be used within StreamingMessageListProvider.
import { useStreamingMessageList } from 'react-native-streaming-message-list';
const { isAtEnd, contentFillsViewport } = useStreamingMessageList();
| Property | Type | Description |
|---|---|---|
isAtEnd |
boolean |
true when scrolled to bottom (within threshold) |
contentFillsViewport |
boolean |
true when content height exceeds viewport |
The component implements ChatGPT-style scrolling by:
See CONTRIBUTING.md
MIT
Made with create-react-native-library
$ claude mcp add react-native-streaming-message-list \
-- python -m otcore.mcp_server <graph>