XDSChatComposerDrawer@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 {XDSChatComposerDrawer} from '@xds/core/Chat'
Props
| Prop | Type | Description |
|---|---|---|
childrenrequired | ReactNode | Content to render inside the drawer — tokens, chips, previews, or any React elements. |
count | number | Total item count shown in the collapsed badge. When provided, the drawer gains a collapse/expand toggle. |
label | string (default: 'Items') | Label shown next to the count in collapsed state. |
isCollapsed | boolean | Controlled collapsed state. Use with `onCollapsedChange` for external control. |
defaultIsCollapsed | boolean (default: false) | Initial collapsed state for uncontrolled usage. |
onCollapsedChange | (isCollapsed: boolean) => void | Callback fired when the user toggles the drawer. |
Examples
Common configurations, variations, and states.ChatComposerDrawer — AttachmentsDrawer with two rows: a scrollable carousel of image thumbnails and a row of removable file tokens. Omit count to keep the drawer always expanded.
tsx'use client';import {XDSChatComposer, XDSChatComposerDrawer} from '@xds/core/Chat';import {XDSToken} from '@xds/core/Token';import {XDSThumbnail} from '@xds/core/Thumbnail';import {XDSCarousel} from '@xds/core/Carousel';import {XDSStack} from '@xds/core/Layout';const IMAGE_ATTACHMENTS = [{id: '1',src: 'https://lookaside.facebook.com/assets/xds_oss/illustrative-vertical-1.jpg',alt: 'River through a valley',label: 'valley.jpg',},{id: '2',src: 'https://lookaside.facebook.com/assets/xds_oss/illustrative-vertical-2.jpg',alt: 'Foggy mountain peak',label: 'mountain.jpg',},{id: '3',src: 'https://lookaside.facebook.com/assets/xds_oss/illustrative-vertical-3.jpg',alt: 'Golden retriever puppy',label: 'puppy.jpg',},{id: '4',src: 'https://lookaside.facebook.com/assets/xds_oss/illustrative-vertical-4.jpg',alt: 'Bridge at sunset',label: 'bridge.jpg',},{id: '5',src: 'https://lookaside.facebook.com/assets/xds_oss/illustrative-vertical-5.jpg',alt: 'Lakeside at dusk',label: 'lakeside.jpg',},];export default function ChatComposerDrawerAttachments() {return (<XDSStack direction="vertical" gap={4} width={480}><XDSChatComposeronSubmit={() => {}}drawer={<XDSChatComposerDrawer><XDSStack direction="vertical" gap={2} width="100%"><XDSCarousel gap={1}>{IMAGE_ATTACHMENTS.map(img => (<XDSThumbnailkey={img.id}src={img.src}alt={img.alt}label={img.label}onRemove={() => {}}/>))}</XDSCarousel><XDSStack direction="horizontal" gap={1} wrap="wrap"><XDSToken label="quarterly-report.pdf" onRemove={() => {}} /><XDSToken label="budget-forecast.xlsx" onRemove={() => {}} /></XDSStack></XDSStack></XDSChatComposerDrawer>}/></XDSStack>);}
ChatComposerDrawer — CollapsibleDrawer with many items and a collapse toggle. Pass count to enable the toggle — collapsed state shows a badge with the total count and a label.
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 ChatComposerDrawerCollapsible() {return (<XDSStack direction="vertical" gap={4} width={480}><XDSChatComposeronSubmit={() => {}}drawer={<XDSChatComposerDrawer count={6} label="Files"><XDSToken label="design-spec.pdf" onRemove={() => {}} /><XDSToken label="api-schema.json" onRemove={() => {}} /><XDSToken label="screenshot.png" onRemove={() => {}} /><XDSToken label="meeting-notes.md" onRemove={() => {}} /><XDSToken label="test-results.csv" onRemove={() => {}} /><XDSToken label="deploy-log.txt" onRemove={() => {}} /></XDSChatComposerDrawer>}/></XDSStack>);}
ChatComposerDrawer — FeedbackChat composer drawer with a feedback prompt and selectable lettered options. Use for user confirmation workflows that require explicit action before proceeding.
tsx'use client';import {useState} from 'react';import {XDSChatComposer, XDSChatComposerDrawer} from '@xds/core/Chat';import {XDSList, XDSListItem} from '@xds/core/List';import {XDSText} from '@xds/core/Text';import {XDSBadge} from '@xds/core/Badge';import {XDSStack} from '@xds/core/Layout';const options = [{key: 'A', label: 'Yes'},{key: 'B', label: 'Yes, and don\u2019t ask again for `git add` commands'},{key: 'C', label: 'No, and tell me what to do differently'},];export default function ChatComposerDrawerFeedback() {const [selected, setSelected] = useState<string | null>(null);return (<XDSStack direction="vertical" style={{width: '100%', maxWidth: 450}}><XDSChatComposeronSubmit={value => {console.log('Submit:', value, '| Answer:', selected);}}drawer={<XDSChatComposerDrawer count={1} label="User feedback requested"><XDSStack direction="vertical" gap={1} width="100%"><XDSList><XDSListItemlabel={<XDSText weight="bold">Do you want to proceed?</XDSText>}/>{options.map(opt => (<XDSListItemkey={opt.key}label={opt.label}startContent={<XDSBadgevariant={selected === opt.key ? 'info' : 'neutral'}label={opt.key}/>}isSelected={selected === opt.key}onClick={() => setSelected(opt.key)}/>))}</XDSList></XDSStack></XDSChatComposerDrawer>}/></XDSStack>);}
ChatComposerDrawer — With ProgressDrawer paired with a context progress bar in the header. Show context window usage when attachments consume part of the available token budget.
tsx'use client';import {XDSChatComposer, XDSChatComposerDrawer} from '@xds/core/Chat';import {XDSToken} from '@xds/core/Token';import {XDSProgressBar} from '@xds/core/ProgressBar';import {XDSStack} from '@xds/core/Layout';import {XDSButton} from '@xds/core/Button';import {XDSIcon} from '@xds/core/Icon';import {PaperClipIcon, AtSymbolIcon} from '@heroicons/react/24/outline';export default function ChatComposerDrawerWithProgress() {return (<XDSStack direction="vertical" gap={4} width={480}><XDSChatComposeronSubmit={() => {}}drawer={<XDSChatComposerDrawer count={3} label="Attachments"><XDSToken label="design-spec.pdf" onRemove={() => {}} /><XDSToken label="api-schema.json" onRemove={() => {}} /><XDSToken label="screenshot.png" onRemove={() => {}} /></XDSChatComposerDrawer>}headerActions={<><XDSButtonlabel="Mention"variant="ghost"size="sm"icon={<XDSIcon icon={AtSymbolIcon} size="sm" />}isIconOnlyonClick={() => {}}/><XDSButtonlabel="Attach"variant="ghost"size="sm"icon={<XDSIcon icon={PaperClipIcon} size="sm" />}isIconOnlyonClick={() => {}}/></>}headerContext={<XDSStack direction="horizontal" gap={2} vAlign="center"><XDSProgressBar value={42} label="Context usage" isLabelHidden hasValueLabel /></XDSStack>}/></XDSStack>);}
Showcase source
tsx'use client';import {XDSChatComposer, XDSChatComposerDrawer} from '@xds/core/Chat';import {XDSToken} from '@xds/core/Token';import {XDSButton} from '@xds/core/Button';import {XDSIcon} from '@xds/core/Icon';import {XDSStack} from '@xds/core/Layout';import {PaperClipIcon} from '@heroicons/react/24/outline';import * as stylex from '@stylexjs/stylex';import {colorVars, borderVars, radiusVars} from '@xds/core/theme/tokens.stylex';const styles = stylex.create({drawerBorder: {border: `${borderVars['--border-width']} solid ${colorVars['--color-border']}`,borderRadius: radiusVars['--radius-page'],},});export default function ChatComposerDrawerShowcase() {return (<XDSStack direction="vertical" gap={4} width={480}><XDSChatComposeronSubmit={() => {}}drawer={<XDSChatComposerDrawer count={4} label="Attachments" xstyle={styles.drawerBorder}><XDSToken label="design-spec.pdf" onRemove={() => {}} /><XDSToken label="api-schema.json" onRemove={() => {}} /><XDSToken label="screenshot.png" onRemove={() => {}} /><XDSToken label="meeting-notes.md" onRemove={() => {}} /></XDSChatComposerDrawer>}headerActions={<XDSButtonlabel="Attach"variant="ghost"size="sm"icon={<XDSIcon icon={PaperClipIcon} size="sm" />}isIconOnlyonClick={() => {}}/>}/></XDSStack>);}