How to Use Astryx Chat Components: ChatMessageList and ChatComposerInput
The Astryx chat UI relies on ChatMessageList to render accessible message histories with infinite scroll, while ChatComposer and ChatComposerInput handle rich-text input through a composable slot architecture that supports triggers, tokens, and custom send behaviors.
The facebook/astryx repository provides a sophisticated React-based chat component library designed for building accessible, high-performance conversational interfaces. This guide explains how to integrate ChatMessageList, ChatComposer, and ChatComposerInput to create everything from simple chat windows to complex AI interfaces with mentions and streaming support.
Core Architecture
Astryx implements a three-layer architecture where presentational containers, composition shells, and rich inputs work together through React context.
ChatMessageList
The ChatMessageList component serves as the scrollable container for chat messages. It supplies a density context to all children and manages infinite-scroll behavior through the scrollToTopAction prop.
Key capabilities include:
- Accessibility: Automatically applies
role="log"andaria-live="polite"so screen readers announce new messages correctly. WhenisStreamingis true, it setsaria-busyto prevent repetitive announcements during token-by-token streaming. - Density control: Accepts
densityvalues of'compact','balanced', or'spacious'that flow down to everyChatMessagechild unless overridden. - Infinite scroll: The
scrollToTopActionprop accepts an async function that Astryx wraps in a React transition, showing a spinner at the top while loading historical data.
According to the source in packages/core/src/Chat/ChatMessageList.tsx, the component also accepts a gap prop to override default spacing, useful for independent rows such as LLM event streams.
ChatComposer
The ChatComposer acts as a shell that holds the input area and auxiliary UI elements. It defines named slots including drawer, headerActions, input, footerActions, sendActions, and sendButton, allowing you to swap in custom pieces while preserving layout, elevation, and focus handling.
The composer registers focus controls on the input slot, enabling automatic focus behavior when users interact with the layout. It expects standard form handlers: onSubmit for send actions and onStop for interrupting streaming responses.
ChatComposerInput
The ChatComposerInput provides a rich-text, token-aware editing experience when passed to the input slot of ChatComposer. It supports:
- Trigger menus: Type-ahead menus activated by characters like
@or/ - Inline tokens: Rendered as badges within the text flow
- Message history recall: Arrow-up/down navigation through previous messages
- Mobile optimization: Enforces a 16px font-size floor on touch devices to prevent iOS zoom
The component accepts triggers, maxRows, debounceMs, and hasHistory props, supplying the same contract (value, onChange, onSubmit) that ChatComposer expects.
Data Flow and Composition
The standard implementation follows this pattern:
- Render
ChatMessageListaboveChatComposerwithin aChatLayoutor page container. - Pass
ChatMessageandChatSystemMessagechildren to the list; each reads the density context supplied by the parent. - Supply
ChatComposerInputto theinputslot ofChatComposerfor advanced features, or rely on the default plain textarea. - When the user presses Enter (without Shift),
ChatComposerInputfiresonSubmit, bubbling up toChatComposer.onSubmit, where you append new messages to the list state.
This flow is implemented in packages/core/src/Chat/ChatLayout.tsx, which demonstrates the recommended vertical stack arrangement.
Practical Implementation Examples
Basic Chat UI
Start with a minimal implementation using the default textarea:
import {ChatMessageList, ChatMessage, ChatComposer} from '@astryxdesign/core/Chat';
export default function SimpleChat() {
const [messages, setMessages] = React.useState<
{id: string; sender: 'user' | 'assistant'; text: string}[]
>([]);
const handleSubmit = (value: string) => {
setMessages([...messages, {id: Date.now().toString(), sender: 'user', text: value}]);
};
return (
<>
<ChatMessageList>
{messages.map(m => (
<ChatMessage key={m.id} sender={m.sender}>
<ChatMessageBubble>{m.text}</ChatMessageBubble>
</ChatMessage>
))}
</ChatMessageList>
<ChatComposer onSubmit={handleSubmit} />
</>
);
}
ChatMessageList automatically scrolls to the bottom when children update, and the default ChatComposer requires no additional wiring.
Rich Input with @Mentions
Replace the default input with ChatComposerInput to enable triggers:
import {
ChatMessageList,
ChatMessage,
ChatComposer,
ChatComposerInput,
} from '@astryxdesign/core/Chat';
export default function MentionChat() {
const [messages, setMessages] = React.useState([]);
const handleSubmit = (value: string) => {
setMessages([...messages, {id: Date.now(), sender: 'user', text: value}]);
};
const mentionTrigger = {
triggerChar: '@',
searchSource: async (query: string) => {
const users = ['alice', 'bob', 'carol'];
return users.filter(u => u.includes(query));
},
onSelect: (selected: string) => ({
type: 'mention',
value: selected,
}),
};
return (
<>
<ChatMessageList>
{messages.map(m => (
<ChatMessage key={m.id} sender={m.sender}>
<ChatMessageBubble>{m.text}</ChatMessageBubble>
</ChatMessage>
))}
</ChatMessageList>
<ChatComposer
onSubmit={handleSubmit}
input={
<ChatComposerInput
triggers={[mentionTrigger]}
placeholder="Say something…"
/>
}
/>
</>
);
}
The triggers prop receives an array of trigger definitions. ChatComposerInput serializes tokens (e.g., @alice) into the string value passed to onSubmit.
Infinite Scroll for Message History
Implement loading older messages when scrolling to the top:
import {
ChatMessageList,
ChatMessage,
ChatComposer,
} from '@astryxdesign/core/Chat';
export default function InfiniteChat() {
const [messages, setMessages] = React.useState([]);
const [loadingOlder, setLoadingOlder] = React.useState(false);
const loadOlder = async () => {
setLoadingOlder(true);
await new Promise(r => setTimeout(r, 800));
const older = [
{id: 'old1', sender: 'assistant', text: 'Earlier message 1'},
{id: 'old2', sender: 'assistant', text: 'Earlier message 2'},
];
setMessages(prev => [...older, ...prev]);
setLoadingOlder(false);
};
const handleSubmit = (value: string) => {
setMessages([...messages, {id: Date.now().toString(), sender: 'user', text: value}]);
};
return (
<>
<ChatMessageList
scrollToTopAction={loadOlder}
isStreaming={loadingOlder}
>
{messages.map(m => (
<ChatMessage key={m.id} sender={m.sender}>
<ChatMessageBubble>{m.text}</ChatMessageBubble>
</ChatMessage>
))}
</ChatMessageList>
<ChatComposer onSubmit={handleSubmit} />
</>
);
}
The scrollToTopAction function must return a Promise. Astryx shows a spinner at the top while the promise resolves, as implemented in packages/core/src/Chat/ChatMessageList.tsx.
Accessibility Features
Astryx bakes in several accessibility optimizations:
ChatMessageListappliesrole="log"andaria-live="polite"to ensure screen readers announce new messages without interrupting the user.- During streaming responses, setting
isStreaming={true}addsaria-busyto prevent assistive technology from reading partial tokens. ChatComposerInputprovides an accessible label (defaulting to "Message input") and maintains focus management through the composer context.
Summary
- ChatMessageList provides density context and infinite scroll capabilities via the
scrollToTopActionprop, with built-in ARIA live regions for accessibility. - ChatComposer uses a slot-based architecture that preserves layout and focus handling while allowing customization of headers, footers, and input methods.
- ChatComposerInput supports rich interactions including trigger menus, inline tokens, and mobile optimizations, exposing the same interface as standard textarea inputs.
- Components connect through standard React props (
value,onChange,onSubmit) and context, with source implementations located inpackages/core/src/Chat/.
Frequently Asked Questions
How do I load older messages when the user scrolls to the top?
Pass an async function to the scrollToTopAction prop of ChatMessageList. Astryx automatically wraps this call in a React transition and displays a loading indicator at the top of the list while your function resolves. This is the standard pattern for implementing infinite scroll in chat histories.
Can I use a plain textarea instead of ChatComposerInput?
Yes. ChatComposer uses a plain textarea by default when you don't provide a custom input slot. You only need to import ChatComposerInput when you require advanced features like trigger menus (@ or / mentions), inline token rendering, or message history recall via arrow keys.
How do I implement @mentions or slash commands?
Define trigger objects with triggerChar, searchSource, and onSelect properties, then pass them to the triggers prop of ChatComposerInput. When users type the trigger character, Astryx renders a type-ahead menu populated by the searchSource function. The onSelect callback should return a token object with type and value properties that ChatComposerInput renders as inline badges.
What accessibility features are built into these components?
ChatMessageList automatically applies role="log" and aria-live="polite" to ensure screen readers announce new messages, and supports aria-busy during streaming. ChatComposerInput includes proper labeling and maintains a 16px font-size floor on touch devices to prevent iOS zoom issues. The composer also manages focus coordination between the input field and layout container.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →