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, PromptInput, 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.Composer>
29 <PromptInput onSubmit={send}>{/* … */}</PromptInput>
30 </Chat.Composer>
31</Chat>

Chat owns scroll behavior. Layout comes from [Message](/docs/ai- elements/message) and Reasoning, composed as children.

Chat.Item is the bridge — a neutral wrapper that registers whatever it wraps with the scroller, whether that is a message, a separator, or a tool-call card.

Message bodies are children, so your app composes markdown rendering, entity chips, and embedded cards. No markdown dependency enters the design system. Consumers own messages, streaming, and sending; everything here is presentational.

Usage

Beyond plain layout there are two concerns: what to render for a file the user attached, and how the scroller behaves while a reply streams in.

Attachments

Chat.Attachment renders a file alongside a message. Attachments sit inside the message item, so they scroll and anchor with it.

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: "" },

API Reference

The scroller and the parts that register with it. Every child goes through Chat.Item; the rest are optional.

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 are the text slot ("Latest" by default); the leading icon renders regardless, swap it with leadingIcon or remove it with leadingIcon={null}.

Prop

Type

Chat.Composer

The container under the messages, usually holding a PromptInput. It sits flush against the messages viewport and pads the sides and bottom.

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

Slots

Every rendered part carries a stable data-slot attribute for styling and testing:

SlotElement
chatRoot container
chat-messagesThe scrollable message list
chat-messages-viewportScroll viewport
chat-messages-contentContent inside the viewport
chat-messages-spacerBottom spacer that keeps the newest turn anchored
chat-messages-scrollbarScrollbar
chat-itemOne message in the list
chat-separatorDivider between messages
chat-composerThe input area below the list
chat-jump-button"Jump to latest" button
chat-jump-button-iconIcon inside that button
chat-attachmentAn attachment card
chat-attachment-mediaIts thumbnail or preview
chat-attachment-bodyIts text column
chat-attachment-titleAttachment filename
chat-attachment-descriptionAttachment size or type
chat-attachment-removeIts remove button

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.