React Native message list with ChatGPT or Claude-style smart scrolling for incremental/streaming-like updates
npm install react-native-streaming-message-list
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
- Smart scroll behavior: New messages snap to top with dynamic blank space management
- Streaming-friendly: Handles growing/updating content without scroll jank
- FlatList-like API: Familiar props, works with any message data structure
``sh`
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:
- Basic Example - Simple, minimal implementation showing core functionality
- ChatGPT Example - ChatGPT-inspired UI
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:
`diff
- import { FlatList } from 'react-native';
+ import { StreamingMessageList } from 'react-native-streaming-message-list';
-
keyExtractor={(item) => item.id}
renderItem={renderMessage}
/>
`
Create a state variable to track when messages are actively streaming:
`tsx`
const [isStreaming, setIsStreaming] = useState(false);
Pass it to StreamingMessageList:
`diff`
keyExtractor={(item) => item.id}
renderItem={renderMessage}
+ isStreaming={isStreaming}
/>
To enable smart scrolling, wrap two special messages:
- Last user message: Wrap with AnchorItem (this message will stay near the top)StreamingItem
- Last assistant message: Wrap with (tracks height changes during and after streaming)
`tsx
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 =
if (isLastUserMessage) {
content =
} else if (isLastAssistantMessage) {
content =
}
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:
`tsx
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 =
return (
{content}
);
};
`
See the Reanimated documentation for more animation options.
1. User sends a message ā mark it as AnchorItemisStreaming={true}
2. Assistant starts responding ā set and wrap the new assistant message with StreamingItemisStreaming={false}
3. Assistant finishes ā set
4. Repeat for the next turn
Use useStreamingMessageList to show a button when the user scrolls away from the bottom:
`tsx
import {
StreamingMessageList,
StreamingMessageListProvider,
useStreamingMessageList,
} from 'react-native-streaming-message-list';
const ScrollToBottomButton = ({ listRef }) => {
const { isAtEnd, contentFillsViewport } = useStreamingMessageList();
if (isAtEnd || !contentFillsViewport) return null;
return (
onPress={() => listRef.current?.scrollToEnd({ animated: true })}
>
);
};
const listRef = useRef(null);
`
Main component that wraps your message list with smart scroll behavior.
#### Props
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 |
#### Config Options
`typescript`
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)
};
Wrapper for the message that should be "anchored" near the top when a new conversation turn begins (typically the last user message).
`tsx`
Wrapper for the last assistant message.
`tsx`
Optional provider that enables access to scroll metrics via useStreamingMessageList. Wrap your list and any components that need scroll metrics with this provider.
`tsx`
Hook to access scroll metrics. Must be used within StreamingMessageListProvider.
`tsx
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:
1. Measuring heights: Tracks the "anchor" message (last user message) and "streaming" content (growing assistant response)
2. Dynamic placeholder: Injects blank space at the bottom so the anchor message lands near the top
3. Auto-scrolling: Automatically scrolls to show new messages
4. Debounced updates: Prevents jank during rapid content updates
See CONTRIBUTING.md
MIT
---
Made with create-react-native-library