XDSChatMessage@xds/core · Chat
Preview coming soon

Usage

XDSChatMessageList is the scrollable container for chat messages. It renders children in a flex column with role="log" for accessibility, provides density context to child messages, and supports infinite scroll for loading older messages. Use it inside XDSChatLayout for full-page chat with auto-scroll and composer docking, or standalone for embedded message panels.

Best practices

GuidancePractices
DoCompose messages using MessageList > Message > Bubble for consistent sender-aware styling and density.
DoSet the density prop to control spacing globally — compact for sidebars, balanced for most views, spacious for long-form reading. Individual messages can override.
DoUse the group prop on bubbles (first, middle, last) when a single sender sends multiple consecutive messages — it tightens corner radius to visually connect them.
DoUse XDSChatSystemMessage with variant="divider" for date separators and default for inline status notices like joins, leaves, or topic changes.
DoPut name on the first bubble and metadata on the last bubble in a message so they align with the bubble's inline padding.
DoProvide an emptyState prop so new users see a clear prompt to start a conversation instead of a blank screen.
DoUse the ghost bubble variant for AI-style responses that show rich content like code blocks or markdown without a visible boundary.
Don'tDon't use XDSChatSystemMessage for sender content — it has no avatar, alignment, or bubble. Use XDSChatMessage with a sender role instead.
Don'tDon't put long or multi-line content in a system message — keep it to a single short sentence. If you need more, use a bubble or a card.
Don'tDon't nest XDSChatMessage inside another XDSChatMessage — each message is a standalone article element with its own sender context.
Don'tDon't apply a fixed height directly on the message list — wrap it in a sized container and let the list fill with flex: 1.
Don'tDon't mix filled and ghost bubble variants within the same sender's messages — pick one style per side and use it consistently.
Don'tDon't place metadata or names on both the bubble and the message wrapper — pick one based on whether the content has a bubble boundary.

Anatomy

ElementDescription
Message arearequiredScrollable region for messages. Renders children (typically XDSChatMessageList) in a flex column that pushes content to the bottom when the list is short.
Frosted glass dockrequiredSticky or fixed container at the bottom with a backdrop-blur layer. Houses the scroll button and composer.
Scroll-to-bottom buttonAppears when the user scrolls up or new messages arrive. Defaults to XDSChatLayoutScrollButton; pass null to hide or a custom element to override.
ComposerrequiredThe input area for sending messages, typically XDSChatComposer. Docked at the bottom inside the frosted glass layer.
Empty stateCentered placeholder shown when no messages exist. Use XDSEmptyState for a consistent look.
AvatarA sender avatar rendered beside the message. Typically XDSAvatar with size="small". Hidden for system messages.
NameSender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content.
ContentrequiredThe message body — one or more XDSChatMessageBubble elements, or any free-form ReactNode like images or tool calls.
MetadataTimestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper.

Import

ts
import {XDSChatMessage} from '@xds/core/Chat'

Props

PropTypeDescription
senderrequired
'user' | 'assistant' | 'system'Who sent this message — controls alignment and layout.
childrenrequired
ReactNodeFree-form content: bubbles, asset lists, tool calls, images.
avatar
ReactNodeAvatar element rendered beside the message. Typically XDSAvatar.
name
ReactNodeSender name rendered above the message body. Use when the first child is raw content (not a bubble). If the first child is a bubble, put the name on the bubble's `name` prop instead.
metadata
ReactNodeMetadata rendered below the message body. Use when the last child is raw content (not a bubble). If the last child is a bubble, put metadata on the bubble's `metadata` prop instead.
density
'compact' | 'balanced' | 'spacious'Visual density. Inherited from list context if not set.

Examples

Common configurations, variations, and states.
ChatMessage — Avatar & NameMessages with avatars and sender names. Place the name on the bubble when using bubbles, or on the message wrapper for raw content.
tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
XDSChatMessageMetadata,
} from '@xds/core/Chat';
import {XDSAvatar} from '@xds/core/Avatar';
import {XDSTimestamp} from '@xds/core/Timestamp';
import {XDSText} from '@xds/core/Text';
import {XDSStack} from '@xds/core/Layout';
export default function ChatMessageAvatarName() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSText type="supporting" color="secondary">
Avatar and name on the bubble
</XDSText>
<XDSChatMessageList>
<XDSChatMessage
sender="assistant"
avatar={<XDSAvatar name="Agent" size="small" />}>
<XDSChatMessageBubble
name={
<XDSText type="supporting" weight="semibold" color="secondary">
Agent
</XDSText>
}
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T10:15:00" format="time" />
}
/>
}>
I reviewed the pull request. The changes look solid — clean code and
good test coverage.
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="user">
<XDSChatMessageBubble
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T10:16:00" format="time" />
}
status="read"
/>
}>
Thanks! Merging it now.
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage
sender="assistant"
avatar={<XDSAvatar name="Agent" size="small" />}>
<XDSChatMessageBubble
name={
<XDSText type="supporting" weight="semibold" color="secondary">
Agent
</XDSText>
}
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T10:17:00" format="time" />
}
/>
}>
I can run the deployment pipeline once it lands. Just let me know.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSStack>
);
}
ChatMessage — GhostGhost variant for messages without visible bubble boundaries. Keeps padding for alignment but renders a transparent background, useful for AI-style responses.
tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
XDSChatMessageMetadata,
} from '@xds/core/Chat';
import {XDSTimestamp} from '@xds/core/Timestamp';
import {XDSText} from '@xds/core/Text';
import {XDSStack} from '@xds/core/Layout';
export default function ChatMessageGhost() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSText type="supporting" color="secondary">
Ghost bubbles — no visible boundary
</XDSText>
<XDSChatMessageList>
<XDSChatMessage sender="assistant">
<XDSChatMessageBubble
variant="ghost"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T09:45:00" format="time" />
}
footer={
<XDSText type="supporting" color="secondary">
Claude Opus 4.6
</XDSText>
}
/>
}>
Here is an analysis of your production metrics from last week.
Traffic peaked at 12,400 requests per second on Wednesday, with a
p99 latency of 45ms. Error rate stayed below 0.1% across all
endpoints.
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="user">
<XDSChatMessageBubble>
That looks great. Can you compare it to the week before?
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="assistant">
<XDSChatMessageBubble
variant="ghost"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T09:46:00" format="time" />
}
footer={
<XDSText type="supporting" color="secondary">
Claude Opus 4.6
</XDSText>
}
/>
}>
Compared to the previous week, traffic is up 8% and latency improved
by 3ms. The deployment on Tuesday seems to have helped.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSStack>
);
}
ChatMessage — Multi-BubbleGrouped bubbles using the group prop for corner radius reduction. Use first, middle, and last to visually connect related bubbles from the same sender.
tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
XDSChatMessageMetadata,
} from '@xds/core/Chat';
import {XDSAvatar} from '@xds/core/Avatar';
import {XDSTimestamp} from '@xds/core/Timestamp';
import {XDSText} from '@xds/core/Text';
import {XDSStack} from '@xds/core/Layout';
export default function ChatMessageMultiBubble() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSText type="supporting" color="secondary">
Grouped bubbles with corner radius reduction
</XDSText>
<XDSChatMessageList>
<XDSChatMessage sender="user">
<XDSChatMessageBubble group="first">
I have a couple of questions about the new API.
</XDSChatMessageBubble>
<XDSChatMessageBubble group="middle">
First, how should we handle pagination?
</XDSChatMessageBubble>
<XDSChatMessageBubble
group="last"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T11:00:00" format="time" />
}
status="delivered"
/>
}>
And second, what's the rate limit?
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage
sender="assistant"
avatar={<XDSAvatar name="Agent" size="small" />}>
<XDSChatMessageBubble
group="first"
name={
<XDSText type="supporting" weight="semibold" color="secondary">
Agent
</XDSText>
}>
Great questions! For pagination, use cursor-based with a limit
parameter. The response includes a nextCursor field.
</XDSChatMessageBubble>
<XDSChatMessageBubble
group="last"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T11:01:00" format="time" />
}
/>
}>
Rate limit is 100 requests per minute per API key. You'll get a 429
response with a Retry-After header if you exceed it.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSStack>
);
}

Showcase source

tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
XDSChatMessageMetadata,
} from '@xds/core/Chat';
import {XDSAvatar} from '@xds/core/Avatar';
import {XDSTimestamp} from '@xds/core/Timestamp';
import {XDSText} from '@xds/core/Text';
import {XDSStack} from '@xds/core/Layout';
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
root: {
maxWidth: 600,
},
});
export default function ChatMessageShowcase() {
return (
<XDSStack direction="vertical" gap={4} xstyle={styles.root}>
<XDSChatMessageList>
<XDSChatMessage sender="user">
<XDSChatMessageBubble group="first">
I just pushed the refactored auth module.
</XDSChatMessageBubble>
<XDSChatMessageBubble
group="last"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T14:30:00" format="time" />
}
status="read"
/>
}>
Can you review the token validation changes?
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage
sender="assistant"
avatar={<XDSAvatar name="Agent" size="small" />}
name={
<XDSText type="supporting" weight="semibold" color="secondary">
Agent
</XDSText>
}
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-28T14:31:00" format="time" />
}
footer={
<XDSText type="supporting" color="secondary">
Claude Opus 4.6
</XDSText>
}
/>
}>
<XDSChatMessageBubble variant="ghost">
Looks good — the refresh token rotation is solid and the error
handling covers all the edge cases. Ship it.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSStack>
);
}