XDSChatComposerInput@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 {XDSChatComposerInput} from '@xds/core/Chat'
Props
| Prop | Type | Description |
|---|---|---|
ref | React.Ref<XDSChatComposerInputHandle> | Imperative handle for programmatic control — insertToken, insertText, focus, and getValue. |
value | string | Controlled input value. Pair with onChange for two-way binding. |
onChange | (value: string) => void | Called when the input value changes. The serialized string includes token placeholders. |
placeholder | string (default: 'Type a message…') | Placeholder text shown when the input is empty. |
maxRows | number (default: 8) | Maximum visible rows before the input scrolls. Use a lower value in compact layouts. |
triggers | XDSChatComposerTrigger[] | Trigger definitions for typeahead menus. Each trigger specifies a character (@ or /), a search source, and an onSelect handler that returns the token to insert. |
debounceMs | number (default: 150) | Debounce delay for async search sources to avoid excessive network requests. |
hasHistory | boolean (default: true) | Enable ArrowUp/Down to recall previously submitted messages. |
label | string (default: 'Message input') | Accessible label announced by screen readers. |
isDisabled | boolean (default: false) | Disables the input. Use during streaming or when a prerequisite is unmet. |
onPaste | (event, text) => void | Called when text is pasted. Use to intercept or transform pasted content. |
onFiles | (files: File[]) => void | Called when files are pasted or dropped onto the input. Use to handle attachments. |
onSubmit | (value: string) => void | Called when the user presses Enter without Shift. The serialized value includes token placeholders. |
Examples
Common configurations, variations, and states.ChatComposerInput — ControlledControlled chat input with live value display. Use controlled mode when you need to read or transform the input value outside the composer.
tsx'use client';import {useState} from 'react';import {XDSChatComposer, XDSChatComposerInput} from '@xds/core/Chat';import {XDSStack} from '@xds/core/Layout';import {XDSText} from '@xds/core/Text';export default function ChatComposerInputControlledInput() {const [value, setValue] = useState('');return (<XDSStack direction="vertical" gap={3} style={{width: '100%', maxWidth: 450}}><XDSChatComposeronSubmit={() => setValue('')}value={value}onChange={setValue}input={<XDSChatComposerInputvalue={value}onChange={setValue}placeholder="Type a message..."/>}/><XDSText type="supporting" color="secondary">Value: {JSON.stringify(value)}</XDSText></XDSStack>);}
ChatComposerInput — DisabledComposer in a disabled state. Use when the input should be visible but not interactive, such as during streaming or when a prerequisite is unmet.
tsx'use client';import {XDSChatComposer, XDSChatComposerInput} from '@xds/core/Chat';import {XDSStack} from '@xds/core/Layout';export default function ChatComposerInputDisabled() {return (<XDSStack direction="vertical" style={{width: '100%', maxWidth: 450}}><XDSChatComposeronSubmit={() => {}}isDisabledinput={<XDSChatComposerInput isDisabled placeholder="Input is disabled" />}/></XDSStack>);}
ChatComposerInput — MentionsChat input with an @ trigger that opens a typeahead menu for mentioning users. Selected names appear as inline tokens.
tsx'use client';import {useState} from 'react';import {XDSChatComposer,XDSChatComposerInput,type XDSChatComposerTrigger,} from '@xds/core/Chat';import {createStaticSource} from '@xds/core/Typeahead';import {XDSTypeaheadItem} from '@xds/core/Typeahead';import type {XDSSearchableItem} from '@xds/core/Typeahead';import {XDSStack} from '@xds/core/Layout';import {XDSText} from '@xds/core/Text';const USERS: XDSSearchableItem<{role: string}>[] = [{id: 'cindy', label: 'Cindy Zhang', auxiliaryData: {role: 'Design Systems'}},{id: 'alex', label: 'Alex Johnson', auxiliaryData: {role: 'Frontend'}},{id: 'sam', label: 'Sam Rivera', auxiliaryData: {role: 'Backend'}},{id: 'jordan', label: 'Jordan Lee', auxiliaryData: {role: 'Product'}},];const userSource = createStaticSource(USERS);export default function ChatComposerInputMentionTrigger() {const [value, setValue] = useState('');const mentionTrigger: XDSChatComposerTrigger = {character: '@',searchSource: userSource,renderItem: item => (<XDSTypeaheadItemitem={item}description={(item.auxiliaryData as {role: string})?.role}/>),onSelect: item => ({value: `@${item.id}`,label: item.label,variant: 'blue' as const,}),};return (<XDSStack direction="vertical" gap={3} style={{width: '100%', maxWidth: 450}}><XDSChatComposeronSubmit={() => setValue('')}input={<XDSChatComposerInputvalue={value}onChange={setValue}triggers={[mentionTrigger]}placeholder="Type @ to mention someone..."/>}/><XDSText type="supporting" color="secondary">Value: {JSON.stringify(value)}</XDSText></XDSStack>);}
ChatComposerInput — Multiple TriggersChat input with both @ mentions and / commands. Each trigger type renders tokens in a distinct color so users can tell them apart at a glance.
tsx'use client';import {useState} from 'react';import {XDSChatComposer,XDSChatComposerInput,type XDSChatComposerTrigger,} from '@xds/core/Chat';import {createStaticSource} from '@xds/core/Typeahead';import type {XDSSearchableItem} from '@xds/core/Typeahead';import {XDSStack} from '@xds/core/Layout';import {XDSText} from '@xds/core/Text';const USERS: XDSSearchableItem[] = [{id: 'cindy', label: 'Cindy Zhang'},{id: 'alex', label: 'Alex Johnson'},{id: 'sam', label: 'Sam Rivera'},{id: 'jordan', label: 'Jordan Lee'},];const COMMANDS: XDSSearchableItem[] = [{id: 'summarize', label: 'summarize'},{id: 'translate', label: 'translate'},{id: 'search', label: 'search'},{id: 'code', label: 'code'},];const userSource = createStaticSource(USERS);const commandSource = createStaticSource(COMMANDS);export default function ChatComposerInputMultipleTriggers() {const [value, setValue] = useState('');const mentionTrigger: XDSChatComposerTrigger = {character: '@',searchSource: userSource,onSelect: item => ({value: `@${item.id}`,label: item.label,variant: 'blue' as const,}),};const commandTrigger: XDSChatComposerTrigger = {character: '/',searchSource: commandSource,onSelect: item => ({value: `/${item.label}`,label: `/${item.label}`,variant: 'yellow' as const,}),};return (<XDSStack direction="vertical" gap={3} style={{width: '100%', maxWidth: 450}}><XDSText type="supporting" color="secondary">Type @ for mentions (blue) or / for commands (yellow)</XDSText><XDSChatComposeronSubmit={() => setValue('')}input={<XDSChatComposerInputvalue={value}onChange={setValue}triggers={[mentionTrigger, commandTrigger]}placeholder="Type @ or / ..."/>}/><XDSText type="supporting" color="secondary">Value: {JSON.stringify(value)}</XDSText></XDSStack>);}
ChatComposerInput — Slash CommandsChat input with a / trigger for command selection. Use for AI assistants or bots that support structured commands.
tsx'use client';import {XDSChatComposer,XDSChatComposerInput,type XDSChatComposerTrigger,} from '@xds/core/Chat';import {createStaticSource} from '@xds/core/Typeahead';import {XDSTypeaheadItem} from '@xds/core/Typeahead';import type {XDSSearchableItem} from '@xds/core/Typeahead';import {XDSStack} from '@xds/core/Layout';const COMMANDS: XDSSearchableItem<{description: string}>[] = [{id: 'summarize', label: 'summarize', auxiliaryData: {description: 'Summarize the conversation'}},{id: 'translate', label: 'translate', auxiliaryData: {description: 'Translate text to another language'}},{id: 'search', label: 'search', auxiliaryData: {description: 'Search the web or documents'}},{id: 'code', label: 'code', auxiliaryData: {description: 'Generate or explain code'}},{id: 'help', label: 'help', auxiliaryData: {description: 'Show available commands'}},];const commandSource = createStaticSource(COMMANDS);export default function ChatComposerInputSlashCommands() {const commandTrigger: XDSChatComposerTrigger = {character: '/',searchSource: commandSource,renderItem: item => (<XDSTypeaheadItemitem={item}description={(item.auxiliaryData as {description: string})?.description}/>),onSelect: item => ({value: `/${item.label}`,label: `/${item.label}`,variant: 'yellow' as const,}),};return (<XDSStack direction="vertical" style={{width: '100%', maxWidth: 450}}><XDSChatComposeronSubmit={() => {}}input={<XDSChatComposerInputtriggers={[commandTrigger]}placeholder="Type / for commands..."/>}/></XDSStack>);}
Showcase source
tsx'use client';import {XDSChatComposer, XDSChatComposerInput} from '@xds/core/Chat';import {XDSStack} from '@xds/core/Layout';export default function ChatComposerInputShowcase() {return (<XDSStack direction="vertical" style={{width: '100%', maxWidth: 450}}><XDSChatComposeronSubmit={() => {}}input={<XDSChatComposerInput placeholder="Ask me anything about XDS..." />}/></XDSStack>);}