# How to Use Astryx Chat Components: ChatMessageList and ChatComposerInput

> Learn to use Astryx Chat components ChatMessageList and ChatComposerInput for accessible message histories and rich text input. Explore triggers tokens and custom send behaviors.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-06

---

**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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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:

```tsx
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:

```tsx
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:

```tsx
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`](https://github.com/facebook/astryx/blob/main/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.