Chat

The conversation scroller — live-edge following, turn anchoring, prepend preservation and visibility tracking, plus separators and attachments.
1<Flex
2 direction="column"
3 style={{
4 width: 440,
5 height: 420,
6 border: "0.5px solid var(--rs-color-border-base-primary)",
7 borderRadius: "var(--rs-radius-4)",
8 }}
9>
10 <Chat>
11 <Chat.Messages>
12 <Chat.Separator>Today 1:52 AM</Chat.Separator>
13 <Chat.Item messageId="m1">
14 <Message align="end">
15 <Message.Content>

Anatomy

1import { Chat, Message, Reasoning } from '@raystack/apsara'
2
3<Chat>
4 <Chat.Messages>
5 <Chat.Separator>Today 1:52 AM</Chat.Separator>
6 <Chat.Item messageId="m1" scrollAnchor>
7 <Message align="end">
8 <Message.Content>
9 <Message.Bubble>create a task in design system 2</Message.Bubble>
10 </Message.Content>
11 </Message>
12 </Chat.Item>
13 <Chat.Item messageId="m2">
14 <Message>
15 <Message.Content>
16 <Reasoning duration={10}>
17 <Reasoning.Trigger />
18 <Reasoning.Content>
19 <Reasoning.Step label="Gathering ticket updates" />
20 </Reasoning.Content>
21 </Reasoning>
22 {/* consumer-rendered markdown / chips / cards */}
23 </Message.Content>
24 </Message>
25 </Chat.Item>
26 <Chat.JumpButton />
27 </Chat.Messages>
28</Chat>

Chat owns scroll behavior; layout comes from Message and Reasoning, composed as children. Chat.Item is the bridge: a neutral wrapper that registers whatever it wraps — a message, a separator, a tool-call card — with the scroller. Message bodies are children: markdown rendering, entity chips and embedded cards are composed by your app — no markdown dependency enters the design system. Consumers own messages, streaming and sending; everything here is presentational.

API Reference

Chat

A flex-column container that sizes Chat.Messages against a composer sibling.

Chat.Messages

The smart scroller. It follows streaming output while the reader is at the bottom, stops following once they scroll up, anchors new scrollAnchor items near the top of the viewport (ChatGPT/Linear style), and keeps the reading position stable when older history is prepended.

Prop

Type

Imperative commands are available through actionsRef:

Prop

Type

Chat.Item

The per-item behavior unit. It registers its element with the enclosing Chat.Messages for visibility tracking and scrollToMessage, marks the turn anchor, and applies content-visibility: auto containment so offscreen items skip layout and paint. It wraps anything — it has no opinion about what a message looks like.

Prop

Type

Chat.JumpButton

The floating "↓ Latest" pill. It appears once the reader leaves the live edge and scrolls back down on click. Render it as a child of Chat.Messages; children replace the default arrow-and-label content.

useChatMessages

Hook for components rendered inside Chat.Messages — active-turn indicators, jump-to-message navigation, custom scroll buttons.

Prop

Type

Chat.Separator

A centered label between hairlines — "Today 1:52 AM". Purely presentational; format dates however your app does.

Chat.Attachment

A presentational attachment preview card for PromptInput.Header or message bodies. File picking, drag-drop and validation belong to your app.

Prop

Type

Examples

Attachments

1<Flex direction="column" gap={3} align="start">
2 <Chat.Attachment
3 title="design-spec.pdf"
4 description="1.2 MB"
5 onRemove={() => {}}
6 />
7 <Chat.Attachment
8 title="screenshot.png"
9 description="Uploading…"
10 state="uploading"
11 />
12 <Chat.Attachment
13 title="notes.txt"
14 description="Upload failed"
15 state="error"

Streaming and turn anchoring

Send a message: it anchors near the top while the reply streams below, and the "↓ Latest" pill returns you to the live edge after scrolling up.

1(function StreamingChat() {
2 const [messages, setMessages] = React.useState([
3 { id: "m1", align: "end", text: "What changed this week?" },
4 { id: "m2", align: "start", text: "Three issues moved to done." },
5 ]);
6 const timersRef = React.useRef([]);
7
8 React.useEffect(() => () => timersRef.current.forEach(clearTimeout), []);
9
10 const send = (value) => {
11 const id = "u" + Date.now();
12 setMessages((current) => [
13 ...current,
14 { id, align: "end", text: value, anchor: true },
15 { id: id + "-reply", align: "start", text: "" },

Accessibility

  • The Chat.Messages viewport has role="log" with an accessible label, so new messages are announced politely by screen readers.
  • The jump button removes itself from the tab order (tabindex="-1", aria-hidden) while the reader is already at the live edge.
  • Programmatic scrolling collapses to instant jumps under prefers-reduced-motion: reduce.