XDSChatMessageBubble@xds/core · Chat
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
| Guidance | Practices |
|---|---|
| Do | Compose messages using MessageList > Message > Bubble for consistent sender-aware styling and density. |
| Do | Set the density prop to control spacing globally — compact for sidebars, balanced for most views, spacious for long-form reading. Individual messages can override. |
| Do | Use the group prop on bubbles (first, middle, last) when a single sender sends multiple consecutive messages — it tightens corner radius to visually connect them. |
| Do | Use XDSChatSystemMessage with variant="divider" for date separators and default for inline status notices like joins, leaves, or topic changes. |
| Do | Put name on the first bubble and metadata on the last bubble in a message so they align with the bubble's inline padding. |
| Do | Provide an emptyState prop so new users see a clear prompt to start a conversation instead of a blank screen. |
| Do | Use the ghost bubble variant for AI-style responses that show rich content like code blocks or markdown without a visible boundary. |
| Don't | Don't use XDSChatSystemMessage for sender content — it has no avatar, alignment, or bubble. Use XDSChatMessage with a sender role instead. |
| Don't | Don'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't | Don't nest XDSChatMessage inside another XDSChatMessage — each message is a standalone article element with its own sender context. |
| Don't | Don'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't | Don't mix filled and ghost bubble variants within the same sender's messages — pick one style per side and use it consistently. |
| Don't | Don'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
| Element | Description | |
|---|---|---|
| Message area | required | Scrollable region for messages. Renders children (typically XDSChatMessageList) in a flex column that pushes content to the bottom when the list is short. |
| Frosted glass dock | required | Sticky or fixed container at the bottom with a backdrop-blur layer. Houses the scroll button and composer. |
| Scroll-to-bottom button | Appears when the user scrolls up or new messages arrive. Defaults to XDSChatLayoutScrollButton; pass null to hide or a custom element to override. | |
| Composer | required | The input area for sending messages, typically XDSChatComposer. Docked at the bottom inside the frosted glass layer. |
| Empty state | Centered placeholder shown when no messages exist. Use XDSEmptyState for a consistent look. | |
| Avatar | A sender avatar rendered beside the message. Typically XDSAvatar with size="small". Hidden for system messages. | |
| Name | Sender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content. | |
| Content | required | The message body — one or more XDSChatMessageBubble elements, or any free-form ReactNode like images or tool calls. |
| Metadata | Timestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper. |
Import
tsimport {XDSChatMessageBubble} from '@xds/core/Chat'
Props
| Prop | Type | Description |
|---|---|---|
childrenrequired | ReactNode | Bubble 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 | ReactNode | Sender 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 | ReactNode | Metadata 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><XDSChatMessagesender="assistant"avatar={<XDSAvatar name="Agent" size="small" />}><XDSChatMessageBubblegroup="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 racecondition on concurrent writes.</XDSChatMessageBubble><XDSChatMessageBubblegroup="last"metadata={<XDSChatMessageMetadatatimestamp={<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><XDSChatMessageBubblegroup="last"metadata={<XDSChatMessageMetadatatimestamp={<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><XDSChatMessagesender="assistant"avatar={<XDSAvatar name="Agent" size="small" />}><XDSChatMessageBubblename={<XDSText type="supporting" weight="semibold" color="secondary">Agent</XDSText>}metadata={<XDSChatMessageMetadatatimestamp={<XDSTimestamp value="2026-04-10T09:15:00" format="time" />}footer={<XDSHStack gap={1}><XDSButtonlabel="Copy"variant="ghost"size="sm"icon={<XDSIcon icon="copy" size="sm" />}isIconOnlyonClick={() => {}}/><XDSText type="supporting" color="secondary">Claude Opus 4.6</XDSText></XDSHStack>}/>}>Your deployment finished successfully. All 14 checks passed.</XDSChatMessageBubble></XDSChatMessage><XDSChatMessage sender="user"><XDSChatMessageBubblemetadata={<XDSChatMessageMetadatatimestamp={<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 allthree 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><XDSChatMessageBubblegroup="last"metadata={<XDSChatMessageMetadatatimestamp={<XDSTimestamp value="2026-04-10T09:15:00" format="time" />}status="read"/>}>Can you review when you get a chance?</XDSChatMessageBubble></XDSChatMessage><XDSChatMessage sender="assistant"><XDSChatMessageBubblevariant="ghost"metadata={<XDSChatMessageMetadatatimestamp={<XDSTimestamp value="2026-04-10T09:16:00" format="time" />}/>}>The changes look great — clean code, good test coverage. Ship it!</XDSChatMessageBubble></XDSChatMessage></XDSChatMessageList></div>);}