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

Props

PropTypeDescription
onSubmitrequired
(value: string) => voidCalled when the user submits a message.
onStop
() => voidCalled when the user requests to stop generation.
isStreaming
boolean (default: false)Whether the assistant is currently streaming.
value
stringControlled input value.
onChange
(value: string) => voidChange handler for controlled mode.
placeholder
string (default: 'Type a message…')Placeholder text shown when the input is empty.
isDisabled
boolean (default: false)Disables the composer.
density
'compact' | 'balanced' | 'spacious' (default: 'balanced')Visual density.
drawer
ReactNodeSlot: collapsible drawer above the input — attachments, context chips, etc. Use XDSChatComposerDrawer.
headerActions
ReactNodeSlot: left-aligned header actions (attach, mention buttons). Use icon-only size="sm" buttons.
headerContext
ReactNodeSlot: right-aligned contextual info in the header (context window usage, XDSProgressBar, supporting text).
input
ReactNodeSlot: custom input element. Replaces the default textarea. Use XDSChatComposerInput for trigger menus.
footerActions
ReactNodeSlot: left-aligned footer actions (model selector, etc).
sendActions
ReactNodeSlot: actions to the left of the send button.
sendButton
ReactNodeSlot: custom send button. Replaces the default send/stop button.
status
{ type: 'error' | 'warning'; message?: string }Status message rendered below (or above) the composer.
statusPosition
'top' | 'bottom' (default: 'bottom')Where to render the status.

Examples

Common configurations, variations, and states.
ChatComposer — AttachmentsChat composer with removable file tokens in a collapsible drawer. Use when users can attach files or context to their message.
tsx
'use client';
import {XDSChatComposer, XDSChatComposerDrawer} from '@xds/core/Chat';
import {XDSToken} from '@xds/core/Token';
import {XDSStack} from '@xds/core/Layout';
export default function ChatComposerAttachments() {
return (
<XDSStack direction="vertical" style={{width: '100%', maxWidth: 450}}>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
}}
drawer={
<XDSChatComposerDrawer count={6}>
<XDSToken label="feature-prd.docx" onRemove={() => {}} />
<XDSToken label="2026-roadmap.pdf" onRemove={() => {}} />
<XDSToken label="user-flow.fig" onRemove={() => {}} />
<XDSToken label="launch-plan.docx" onRemove={() => {}} />
<XDSToken label="user-feedback.csv" onRemove={() => {}} />
<XDSToken label="analytics-kpis.csv" onRemove={() => {}} />
</XDSChatComposerDrawer>
}
/>
</XDSStack>
);
}
ChatComposer — Footer ActionsChat composer with dropdown menus for a model selector and settings in the footer, and a mic button in the send actions slot.
tsx
'use client';
import {XDSChatComposer} from '@xds/core/Chat';
import {XDSButton} from '@xds/core/Button';
import {XDSDropdownMenu} from '@xds/core/DropdownMenu';
import {XDSIcon} from '@xds/core/Icon';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
import {
Cog6ToothIcon,
MicrophoneIcon,
SparklesIcon,
} from '@heroicons/react/24/outline';
export default function ChatComposerFooterActions() {
return (
<XDSStack direction="vertical" gap={4} style={{width: '100%', maxWidth: 450}}>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Model selector and settings dropdowns
</XDSText>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
}}
footerActions={
<>
<XDSDropdownMenu
button={{
label: 'Auto',
variant: 'ghost',
size: 'md',
icon: <XDSIcon icon={SparklesIcon} size="sm" />,
children: 'Auto',
}}
menuWidth={200}
items={[
{label: 'Auto', onClick: () => {}},
{label: 'Model A', onClick: () => {}},
{label: 'Model B', onClick: () => {}},
{label: 'Model C', onClick: () => {}},
]}
/>
<XDSDropdownMenu
button={{
label: 'Settings',
variant: 'ghost',
size: 'md',
icon: <XDSIcon icon={Cog6ToothIcon} size="sm" />,
children: 'Settings',
}}
menuWidth={200}
items={[
{label: 'Preferences', onClick: () => {}},
{label: 'Keyboard shortcuts', onClick: () => {}},
{label: 'About', onClick: () => {}},
]}
/>
</>
}
sendActions={
<XDSButton
label="Microphone"
variant="ghost"
size="md"
icon={<XDSIcon icon={MicrophoneIcon} />}
isIconOnly
/>
}
/>
</XDSStack>
</XDSStack>
);
}
ChatComposer — Full FeaturedChat composer with all slots populated — collapsible attachment drawer, header actions, context progress bar, footer dropdown menus, and mic button. Shows the maximum composer configuration.
tsx
'use client';
import {useState} from 'react';
import {XDSChatComposer, XDSChatComposerDrawer} from '@xds/core/Chat';
import {XDSToken} from '@xds/core/Token';
import {XDSButton} from '@xds/core/Button';
import {XDSDropdownMenu} from '@xds/core/DropdownMenu';
import {XDSIcon} from '@xds/core/Icon';
import {XDSProgressBar} from '@xds/core/ProgressBar';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
import {
AtSymbolIcon,
Cog6ToothIcon,
MicrophoneIcon,
PaperClipIcon,
SparklesIcon,
} from '@heroicons/react/24/outline';
export default function ChatComposerFullFeatured() {
const [isStreaming, setIsStreaming] = useState(false);
return (
<XDSStack direction="vertical" gap={4} style={{width: '100%', maxWidth: 450}}>
<XDSText type="supporting" color="secondary">
All slots populated
</XDSText>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
setIsStreaming(true);
setTimeout(() => setIsStreaming(false), 3000);
}}
isStreaming={isStreaming}
onStop={() => setIsStreaming(false)}
placeholder="Ask me anything..."
drawer={
<XDSChatComposerDrawer count={5}>
<XDSToken label="design-spec.pdf" onRemove={() => {}} />
<XDSToken label="requirements.docx" onRemove={() => {}} />
<XDSToken label="wireframes.fig" onRemove={() => {}} />
<XDSToken label="api-spec.yaml" onRemove={() => {}} />
<XDSToken label="user-research.csv" onRemove={() => {}} />
</XDSChatComposerDrawer>
}
headerActions={
<>
<XDSButton
label="Mention"
variant="ghost"
size="sm"
icon={<XDSIcon icon={AtSymbolIcon} />}
isIconOnly
/>
<XDSButton
label="Attach file"
variant="ghost"
size="sm"
icon={<XDSIcon icon={PaperClipIcon} />}
isIconOnly
/>
</>
}
headerContext={
<XDSProgressBar label="Context window" value={3} isLabelHidden />
}
footerActions={
<>
<XDSDropdownMenu
button={{
label: 'Auto',
variant: 'ghost',
size: 'md',
icon: <XDSIcon icon={SparklesIcon} size="sm" />,
children: 'Auto',
}}
menuWidth={200}
items={[
{label: 'Auto', onClick: () => {}},
{label: 'Model A', onClick: () => {}},
{label: 'Model B', onClick: () => {}},
{label: 'Model C', onClick: () => {}},
]}
/>
<XDSDropdownMenu
button={{
label: 'Settings',
variant: 'ghost',
size: 'md',
icon: <XDSIcon icon={Cog6ToothIcon} size="sm" />,
children: 'Settings',
}}
menuWidth={200}
items={[
{label: 'Preferences', onClick: () => {}},
{label: 'Keyboard shortcuts', onClick: () => {}},
{label: 'About', onClick: () => {}},
]}
/>
</>
}
sendActions={
<XDSButton
label="Microphone"
variant="ghost"
size="md"
icon={<XDSIcon icon={MicrophoneIcon} />}
isIconOnly
/>
}
/>
</XDSStack>
);
}
ChatComposer — SimpleMinimal chat composer with a placeholder and submit handler. The simplest way to drop a message input into a page.
tsx
'use client';
import {XDSChatComposer} from '@xds/core/Chat';
import {XDSStack} from '@xds/core/Layout';
export default function ChatComposerSimple() {
return (
<XDSStack direction="vertical" style={{width: '100%', maxWidth: 450}}>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
}}
/>
</XDSStack>
);
}
ChatComposer — StreamingChat composer with streaming state and a stop button. Use when the assistant is generating a response and the user can cancel.
tsx
'use client';
import {useState} from 'react';
import {XDSChatComposer} from '@xds/core/Chat';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
export default function ChatComposerStreaming() {
const [isStreaming, setIsStreaming] = useState(false);
return (
<XDSStack direction="vertical" gap={4} style={{width: '100%', maxWidth: 450}}>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
{isStreaming ? 'Streaming — click stop to cancel' : 'Send a message to start streaming'}
</XDSText>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
setIsStreaming(true);
setTimeout(() => setIsStreaming(false), 5000);
}}
isStreaming={isStreaming}
onStop={() => {
console.log('Stopped');
setIsStreaming(false);
}}
placeholder="Send a message to start streaming..."
/>
</XDSStack>
</XDSStack>
);
}
ChatComposer — ValidationChat composer with error and warning status messages. Status can appear above or below the composer to surface validation or system feedback.
tsx
'use client';
import {XDSChatComposer} from '@xds/core/Chat';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
export default function ChatComposerValidation() {
return (
<XDSStack direction="vertical" gap={4} style={{width: '100%', maxWidth: 450}}>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Error message (with top position)
</XDSText>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
}}
statusPosition="top"
status={{
type: 'error',
message: 'Failed to send message. Please try again.',
}}
/>
</XDSStack>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Warning message (with bottom position)
</XDSText>
<XDSChatComposer
onSubmit={value => {
console.log('Sent:', value);
}}
status={{
type: 'warning',
message: 'Context window is 90% full.',
}}
/>
</XDSStack>
</XDSStack>
);
}

Showcase source

tsx
'use client';
import {XDSChatComposer} from '@xds/core/Chat';
import {XDSStack} from '@xds/core/Layout';
export default function ChatComposerShowcase() {
return (
<XDSStack direction="vertical" width="100%" style={{maxWidth: 450}}>
<XDSChatComposer
onSubmit={() => {}}
placeholder="Type a message…"
/>
</XDSStack>
);
}