UI Primitives for building AI apps in React Native.
aix is currently in alpha preview. The API is likely to change.
- Start a chat scrolled to end on the first frame
- Animate scrolling to new messages when they send
- Float messages to the top of the screen with automated "blank size" handling
- Animate message content as it streams
- Keyboard handling out-of-the-box with no external dependencies
- Support for absolute-positioned composers
- Detect "is scrolled near end" for ScrollToEnd buttons
To learn about the motivation behind AIX, you can read our blog post on How we built the v0 iOS app. AIX is an opinionated, feature-complete, and extensible way to implement every single feature mentioned in that blog post.
When building AIX, we started by copying the code from v0 into a separate repository. However, as we worked to make it flexible for use cases outside of our own app, we decided to rewrite it from scratch in native code. What you see here is a Nitro Module which handles all business logic in UIKit. We plan on adding support for Android as well and welcome contributions.
npm i aix react-native-nitro-modulesNext, rebuild your native app. For Expo users, run npx expo prebuild and
rebuild.
- For a full example, see the example app.
Wrap your ScrollView with Aix, and wrap your messages with AixCell.
import { Aix, AixCell } from 'aix'
import { Message } from 'path/to/your/message'
import { Composer } from 'path/to/your/composer'
export function ChatScreen({ messages }) {
return (
<Aix style={{ flex: 1 }} shouldStartAtEnd>
<ScrollView>
{messages.map((message) => (
<AixCell
key={message.id}
index={index}
isLast={index === messages.length - 1}
>
<Message message={message} />
</AixCell>
))}
</ScrollView>
</Aix>
)
}To add a floating composer which lets content scroll under it, you can use the
AixFooter. Pair it with Aix.scrollOnFooterSizeUpdate to ensure content
scrolls under the footer when it changes size.
import { Aix, AixCell, AixFooter } from 'aix'
export function ChatScreen({ messages }) {
const { bottom } = useSafeAreaInsets()
return (
<Aix
style={{ flex: 1 }}
shouldStartAtEnd
scrollOnFooterSizeUpdate={{
enabled: true,
scrolledToEndThreshold: 100,
animated: false,
}}
>
<ScrollView>
{messages.map((message) => (
<AixCell
key={message.id}
index={index}
isLast={index === messages.length - 1}
>
<Message message={message} />
</AixCell>
))}
</ScrollView>
<AixFooter
style={{ position: 'absolute', inset: 0, top: 'auto' }}
stickToKeyboard={{
enabled: true,
offset: {
whenKeyboardOpen: 0,
whenKeyboardClosed: -bottom,
},
}}
>
<Composer />
</AixFooter>
</Aix>
)
}When sending a message, you will likely want to scroll to it after it gets added to the list.
Simply call aix.current?.scrollToIndexWhenBlankSizeReady(index) in your submit
handler.
The index you pass should correspond to the newest message in the list. For AI
chats, this is typically the next assistant message index.
import { Keyboard } from 'react-native'
import { useAixRef } from 'aix'
function Chat() {
const aix = useAixRef()
const onSubmit = () => {
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
requestAnimationFrame(Keyboard.dismiss)
}
return <Aix ref={aix}>{/* ... */}</Aix>
}You can use onScrolledNearEndChange to show a "scroll to end" button when the
user scrolls away from the bottom:
import { Aix, useAixRef } from 'aix'
import { useState } from 'react'
import { Button } from 'react-native'
function Chat() {
const aix = useAixRef()
const [isNearEnd, setIsNearEnd] = useState(false)
return (
<Aix
ref={aix}
scrollEndReachedThreshold={200}
onScrolledNearEndChange={setIsNearEnd}
>
{/* ScrollView and messages... */}
{!isNearEnd && (
<Button
onPress={() => aix.current?.scrollToEnd(true)}
title='Scroll to end'
/>
)}
</Aix>
)
}The main container component that provides keyboard-aware behavior and manages scrolling for chat interfaces.
| Prop | Type | Default | Description |
|---|---|---|---|
shouldStartAtEnd |
boolean |
- | Whether the scroll view should start scrolled to the end of the content. |
scrollOnFooterSizeUpdate |
object |
{ enabled: true, scrolledToEndThreshold: 100, animated: false } |
Control the behavior of scrolling when the footer size changes. By default, changing the height of the footer will shift content up in the scroll view. |
scrollEndReachedThreshold |
number |
max(blankSize, 200) |
The number of pixels from the bottom of the scroll view to the end of the content that is considered "near the end". Used by onScrolledNearEndChange and to determine if content should shift up when keyboard opens. |
onScrolledNearEndChange |
(isNearEnd: boolean) => void |
- | Callback fired when the scroll position transitions between "near end" and "not near end" states. Reactive to user scrolling, content size changes, parent size changes, and keyboard height changes. Uses scrollEndReachedThreshold to determine the threshold. |
additionalContentInsets |
object |
- | Additional content insets applied when keyboard is open or closed. Shape: { top?: { whenKeyboardOpen, whenKeyboardClosed }, bottom?: { whenKeyboardOpen, whenKeyboardClosed } } |
additionalScrollIndicatorInsets |
object |
- | Additional insets for the scroll indicator, added to existing safe area insets. Applied to verticalScrollIndicatorInsets on iOS. |
mainScrollViewID |
string |
- | The nativeID of the scroll view to use. If provided, will search for a scroll view with this accessibilityIdentifier. |
penultimateCellIndex |
number |
- | The index of the second-to-last message (typically the last user message in AI chat apps). Used to determine which message will be scrolled into view. Useful when you have custom message types like timestamps in your list. |
Access these methods via useAixRef():
const aix = useAixRef()
// Scroll to the end of the content
aix.current?.scrollToEnd(animated)
// Scroll to a specific index when the blank size is ready
aix.current?.scrollToIndexWhenBlankSizeReady(
index,
animated,
waitForKeyboardToEnd
)| Method | Parameters | Description |
|---|---|---|
scrollToEnd |
animated?: boolean |
Scrolls to the end of the content. |
scrollToIndexWhenBlankSizeReady |
index: number, animated?: boolean, waitForKeyboardToEnd?: boolean |
Scrolls to a specific cell index once the blank size calculation is ready. |
A wrapper component for each message in the list. It communicates cell position
and dimensions to the parent Aix component.
| Prop | Type | Required | Description |
|---|---|---|---|
index |
number |
Yes | The index of this cell in the message list. |
isLast |
boolean |
Yes | Whether this cell is the last item in the list. Used for scroll positioning and animations. |
A footer component for floating composers that allows content to scroll underneath it. The footer's height is automatically tracked for proper scroll offset calculations.
Accepts all standard React Native View props.
- Do not apply vertical padding (
padding,paddingBottom) directly toAixFooter. Apply padding to a child view instead. - Position the footer absolutely at the bottom of the
Aixcontainer:
<AixFooter style={{ position: 'absolute', inset: 0, top: 'auto' }}>
<Composer />
</AixFooter>A hook that returns a ref to access imperative methods on the Aix component.
import { useAixRef } from 'aix'
function Chat({ messages }) {
const aix = useAixRef()
const send = useSendMessage()
const handleSend = () => {
// Scroll to end after sending a message
send(message)
aix.current?.scrollToIndexWhenBlankSizeReady(messages.length + 1, true)
requestAnimationFrame(Keyboard.dismiss)
}
return <Aix ref={aix}>{/* ... */}</Aix>
}- Do NOT apply padding to
contentContainerStyle. Instead, use padding on children directly. - Do NOT apply padding to
AixFooter. Instead, use padding on its children
- Android support
- LegendList support
- FlashList support
- React Native v0.78.0 or higher
- Node 18.0.0 or higher
