XDSChatComposer@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 {XDSChatComposer} from '@xds/core/Chat'
Props
| Prop | Type | Description |
|---|---|---|
onSubmitrequired | (value: string) => void | Called when the user submits a message. |
onStop | () => void | Called when the user requests to stop generation. |
isStreaming | boolean (default: false) | Whether the assistant is currently streaming. |
value | string | Controlled input value. |
onChange | (value: string) => void | Change 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 | ReactNode | Slot: collapsible drawer above the input — attachments, context chips, etc. Use XDSChatComposerDrawer. |
headerActions | ReactNode | Slot: left-aligned header actions (attach, mention buttons). Use icon-only size="sm" buttons. |
headerContext | ReactNode | Slot: right-aligned contextual info in the header (context window usage, XDSProgressBar, supporting text). |
input | ReactNode | Slot: custom input element. Replaces the default textarea. Use XDSChatComposerInput for trigger menus. |
footerActions | ReactNode | Slot: left-aligned footer actions (model selector, etc). |
sendActions | ReactNode | Slot: actions to the left of the send button. |
sendButton | ReactNode | Slot: 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}}><XDSChatComposeronSubmit={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><XDSChatComposeronSubmit={value => {console.log('Sent:', value);}}footerActions={<><XDSDropdownMenubutton={{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: () => {}},]}/><XDSDropdownMenubutton={{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={<XDSButtonlabel="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><XDSChatComposeronSubmit={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={<><XDSButtonlabel="Mention"variant="ghost"size="sm"icon={<XDSIcon icon={AtSymbolIcon} />}isIconOnly/><XDSButtonlabel="Attach file"variant="ghost"size="sm"icon={<XDSIcon icon={PaperClipIcon} />}isIconOnly/></>}headerContext={<XDSProgressBar label="Context window" value={3} isLabelHidden />}footerActions={<><XDSDropdownMenubutton={{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: () => {}},]}/><XDSDropdownMenubutton={{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={<XDSButtonlabel="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}}><XDSChatComposeronSubmit={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><XDSChatComposeronSubmit={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><XDSChatComposeronSubmit={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><XDSChatComposeronSubmit={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}}><XDSChatComposeronSubmit={() => {}}placeholder="Type a message…"/></XDSStack>);}