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" and aria-live="polite" so screen readers announce new messages correctly. When isStreaming is true, it sets aria-busy to prevent repetitive announcements during token-by-token streaming.
  • Density control: Accepts density values of 'compact', 'balanced', or 'spacious' that flow down to every ChatMessage child unless overridden.
  • Infinite scroll: The scrollToTopAction prop 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:

  1. Render ChatMessageList above ChatComposer within a ChatLayout or page container.
  2. Pass ChatMessage and ChatSystemMessage children to the list; each reads the density context supplied by the parent.
  3. Supply ChatComposerInput to the input slot of ChatComposer for advanced features, or rely on the default plain textarea.
  4. When the user presses Enter (without Shift), ChatComposerInput fires onSubmit, bubbling up to ChatComposer.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:

  • ChatMessageList applies role="log" and aria-live="polite" to ensure screen readers announce new messages without interrupting the user.
  • During streaming responses, setting isStreaming={true} adds aria-busy to prevent assistive technology from reading partial tokens.
  • ChatComposerInput provides 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 scrollToTopAction prop, 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 in packages/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →