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

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

Props

PropTypeDescription
childrenrequired
ReactNodeContent to render inside the drawer — tokens, chips, previews, or any React elements.
count
numberTotal 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
booleanControlled collapsed state. Use with `onCollapsedChange` for external control.
defaultIsCollapsed
boolean (default: false)Initial collapsed state for uncontrolled usage.
onCollapsedChange
(isCollapsed: boolean) => voidCallback 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}>
<XDSChatComposer
onSubmit={() => {}}
drawer={
<XDSChatComposerDrawer>
<XDSStack direction="vertical" gap={2} width="100%">
<XDSCarousel gap={1}>
{IMAGE_ATTACHMENTS.map(img => (
<XDSThumbnail
key={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}>
<XDSChatComposer
onSubmit={() => {}}
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}}>
<XDSChatComposer
onSubmit={value => {
console.log('Submit:', value, '| Answer:', selected);
}}
drawer={
<XDSChatComposerDrawer count={1} label="User feedback requested">
<XDSStack direction="vertical" gap={1} width="100%">
<XDSList>
<XDSListItem
label={
<XDSText weight="bold">Do you want to proceed?</XDSText>
}
/>
{options.map(opt => (
<XDSListItem
key={opt.key}
label={opt.label}
startContent={
<XDSBadge
variant={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}>
<XDSChatComposer
onSubmit={() => {}}
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={
<>
<XDSButton
label="Mention"
variant="ghost"
size="sm"
icon={<XDSIcon icon={AtSymbolIcon} size="sm" />}
isIconOnly
onClick={() => {}}
/>
<XDSButton
label="Attach"
variant="ghost"
size="sm"
icon={<XDSIcon icon={PaperClipIcon} size="sm" />}
isIconOnly
onClick={() => {}}
/>
</>
}
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}>
<XDSChatComposer
onSubmit={() => {}}
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={
<XDSButton
label="Attach"
variant="ghost"
size="sm"
icon={<XDSIcon icon={PaperClipIcon} size="sm" />}
isIconOnly
onClick={() => {}}
/>
}
/>
</XDSStack>
);
}