XDSChatMessageBubble@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 {XDSChatMessageBubble} from '@xds/core/Chat'

Props

PropTypeDescription
childrenrequired
ReactNodeBubble content — text, XDSMarkdown, or any ReactNode.
variant
'filled' | 'ghost' (default: 'filled')Visual variant. 'filled' renders sender-colored background (default). 'ghost' renders transparent background but keeps padding for alignment.
name
ReactNodeSender name rendered above the bubble, aligned with bubble text padding. Use on the first bubble in a message. If the first content is raw (no bubble), use XDSChatMessage's `name` prop instead.
metadata
ReactNodeMetadata content rendered below the bubble, aligned with bubble text padding. Use on the last bubble in a message. If the last content is raw (no bubble), use XDSChatMessage's `metadata` prop instead.
group
'first' | 'middle' | 'last'Position within a multi-bubble group. Controls corner radius reduction on the sender side. Leave unset for standalone bubbles (full radius).

Examples

Common configurations, variations, and states.
ChatMessageBubble — DensityCompact, balanced, and spacious density modes side by side. Density controls bubble padding, corner radius, and spacing between grouped bubbles.
tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
} from '@xds/core/Chat';
import {XDSText} from '@xds/core/Text';
import {XDSVStack} from '@xds/core/Layout';
const DENSITIES = [
{density: 'compact' as const, label: 'Compact'},
{density: 'balanced' as const, label: 'Balanced'},
{density: 'spacious' as const, label: 'Spacious'},
];
export default function ChatMessageBubbleDensity() {
return (
<XDSVStack gap={5}>
{DENSITIES.map(({density, label}) => (
<XDSVStack key={density} gap={1}>
<XDSText type="supporting" color="secondary">
{label}
</XDSText>
<XDSChatMessageList density={density}>
<XDSChatMessage sender="assistant">
<XDSChatMessageBubble>
The build completed in 4.2 seconds.
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="user">
<XDSChatMessageBubble>
Ship it to staging.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSVStack>
))}
</XDSVStack>
);
}
ChatMessageBubble — GroupingMulti-bubble messages using first, middle, and last group positions. Grouped bubbles tighten corner radius on the sender side for a continuous visual flow.
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 {XDSVStack} from '@xds/core/Layout';
export default function ChatMessageBubbleGrouping() {
return (
<XDSVStack gap={4}>
<XDSText type="supporting" color="secondary">
Grouped bubbles with tightened sender-side corners
</XDSText>
<XDSChatMessageList>
<XDSChatMessage
sender="assistant"
avatar={<XDSAvatar name="Agent" size="small" />}>
<XDSChatMessageBubble
group="first"
name={
<XDSText type="supporting" weight="semibold" color="secondary">
Agent
</XDSText>
}>
I reviewed the three files you shared.
</XDSChatMessageBubble>
<XDSChatMessageBubble group="middle">
The data model looks solid, but the API handler has a race
condition on concurrent writes.
</XDSChatMessageBubble>
<XDSChatMessageBubble
group="last"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-10T10:45:00" format="time" />
}
/>
}>
I can draft a fix if you want.
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="user">
<XDSChatMessageBubble group="first">
Yes please!
</XDSChatMessageBubble>
<XDSChatMessageBubble
group="last"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-10T10:46:00" format="time" />
}
status="delivered"
/>
}>
Also add a test for the concurrent case.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSVStack>
);
}
ChatMessageBubble — MetadataBubbles with name and metadata slots aligned to bubble padding. Put name on the first bubble and metadata on the last bubble in a message.
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 {XDSButton} from '@xds/core/Button';
import {XDSIcon} from '@xds/core/Icon';
import {XDSHStack, XDSVStack} from '@xds/core/Layout';
export default function ChatMessageBubbleMetadata() {
return (
<XDSVStack gap={4}>
<XDSText type="supporting" color="secondary">
Name on first bubble, metadata on last
</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-10T09:15:00" format="time" />
}
footer={
<XDSHStack gap={1}>
<XDSButton
label="Copy"
variant="ghost"
size="sm"
icon={<XDSIcon icon="copy" size="sm" />}
isIconOnly
onClick={() => {}}
/>
<XDSText type="supporting" color="secondary">
Claude Opus 4.6
</XDSText>
</XDSHStack>
}
/>
}>
Your deployment finished successfully. All 14 checks passed.
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="user">
<XDSChatMessageBubble
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-10T09:16:00" format="time" />
}
status="read"
/>
}>
Great, can you send me the production URL?
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSVStack>
);
}
ChatMessageBubble — VariantsFilled and ghost bubble variants for both user and assistant senders. Use filled for standard messages and ghost when content needs alignment without a visual boundary.
tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
} from '@xds/core/Chat';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
export default function ChatMessageBubbleVariants() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Filled — sender-colored background (default)
</XDSText>
<XDSChatMessageList>
<XDSChatMessage sender="user">
<XDSChatMessageBubble>
Can you summarize the latest deployment logs?
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSStack>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Ghost — transparent background, keeps alignment padding
</XDSText>
<XDSChatMessageList>
<XDSChatMessage sender="assistant">
<XDSChatMessageBubble variant="ghost">
The last deploy completed at 2:41 PM with zero errors across all
three regions.
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</XDSStack>
</XDSStack>
);
}

Showcase source

tsx
'use client';
import {
XDSChatMessageList,
XDSChatMessage,
XDSChatMessageBubble,
XDSChatMessageMetadata,
} from '@xds/core/Chat';
import {XDSTimestamp} from '@xds/core/Timestamp';
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
root: {
maxWidth: 600,
},
});
export default function ChatMessageBubbleShowcase() {
return (
<div {...stylex.props(styles.root)}>
<XDSChatMessageList>
<XDSChatMessage sender="user">
<XDSChatMessageBubble group="first">
I just pushed the latest changes to the feature branch.
</XDSChatMessageBubble>
<XDSChatMessageBubble
group="last"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-10T09:15:00" format="time" />
}
status="read"
/>
}>
Can you review when you get a chance?
</XDSChatMessageBubble>
</XDSChatMessage>
<XDSChatMessage sender="assistant">
<XDSChatMessageBubble
variant="ghost"
metadata={
<XDSChatMessageMetadata
timestamp={
<XDSTimestamp value="2026-04-10T09:16:00" format="time" />
}
/>
}>
The changes look great — clean code, good test coverage. Ship it!
</XDSChatMessageBubble>
</XDSChatMessage>
</XDSChatMessageList>
</div>
);
}