- Accordion
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- ChatContainer
- ChatInput
- ChatMessage
- ChatMultiChoiceQuestion
- ChatMultiOptionQuestion
- ChatThinking
- Checkbox
- Combobox
- Container
- CurrencyInput
- DistributionSlider
- Drawer
- Dropdown
- FilePicker
- Grid
- Heading
- Image
- Input
- InputGroup
- Label
- Logo
- MapPin
- Markdown
- Modal
- NativeSelect
- NumberInput
- OptionSlider
- OtpInput
- PhoneInput
- Popover
- Progress
- PropertyCalendar
- RadioGroup
- RadioGroupCards
- ResponsiveModal
- ScrollArea
- SearchBar
- SearchBarFallback
- SearchInput
- Select
- Separator
- Spinner
- Switch
- Table
- Tabs
- Text
- Textarea
- TimePicker
- Toast
- Toggle
- ToggleCard
- ToggleGroup
- Toolbar
- Tooltip
ChatInput
A composable chat composer with auto-growing textarea, image attachment row, context chips, and a send button.
pnpm add @wandercom/design-system-web
import { ChatInput } from '@wandercom/design-system-web/ui/chat-input';
export function Example() {
const [message, setMessage] = useState('');
return (
<ChatInput
onSubmit={(payload) => send(payload)}
onValueChange={setMessage}
value={message}
/>
);
}The high-level ChatInput composes the media slot (attachments + context chips), textarea, and bottom actions (attach + send) into a single drop-in. Reach for the subcomponents (ChatInputRoot, ChatInputMedia, ChatInputTextarea, etc.) when you need a custom layout.
A bare composer. The textarea auto-grows up to 160px before scrolling internally. Enter inserts a newline; Cmd/Ctrl+Enter submits, as does the send button.
<ChatInput
accept={{ 'image/*': ['.jpg', '.jpeg', '.png'] }}
onSubmit={(payload) => send(payload)}
onValueChange={setMessage}
placeholder="Make edits here"
value={message}
/>Image attachments render in a horizontal scroll row above the textarea, with edge fades aligned to the composer's outer padding. Files dropped through ChatInputAttach are appended automatically; pass a controlled attachments array and onAttachmentsChange to manage the list yourself (useful for loading states, server upload, etc).
<ChatInput
accept={{ 'image/*': ['.jpg', '.jpeg', '.png'] }}
attachments={attachments}
onAttachmentsChange={setAttachments}
onSubmit={(payload) => send(payload)}
onValueChange={setMessage}
value={message}
/>Render small pills above the textarea to expose the chat's active scope — selected formatting tag, referenced calendar, property the agent is operating on. Pass contexts and an onContextRemove callback; the composer renders the chips and wires the remove buttons.
import { IconCalendar1 } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconCalendar1';
import { IconHome } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconHome';
const contexts = [
{ id: 'calendar', label: 'Calendar', icon: <IconCalendar1 /> },
{ id: 'property', label: 'Big Sur Cabin', icon: <IconHome /> },
{ id: 'h1', label: 'H1' },
];
<ChatInput
contexts={contexts}
onContextRemove={(id) => removeContext(id)}
onSubmit={(payload) => send(payload)}
onValueChange={setMessage}
value={message}
/>;For full control over layout and per-row styling, drop down to the subcomponents. The high-level ChatInput is a thin wrapper around exactly this composition.
ChatInputInline is a single-line variant — a 44px input row with the same circular primary send button. Use it wherever the chat needs a compact "type a quick reply" affordance (e.g. an "Other" answer on ChatMultiChoiceQuestion) without the textarea, attachments, or context chip surface area of the full ChatInput. The background fades to surface-input on hover, focus, and while a value is present.
<ChatInputInline
aria-label="Reply"
onSubmit={(value) => respond(value)}
placeholder="Something else"
/><ChatInputRoot
attachments={attachments}
contexts={contexts}
onAttachmentsChange={setAttachments}
onContextRemove={(id) => removeContext(id)}
onSubmit={(payload) => send(payload)}
onValueChange={setMessage}
value={message}
>
<ChatInputMedia>
<ChatInputAttachments />
<ChatInputContexts />
</ChatInputMedia>
<ChatInputTextarea placeholder="Make edits here" />
<ChatInputActions>
<ChatInputAttach />
<ChatInputSend />
</ChatInputActions>
</ChatInputRoot>The send button is disabled when the trimmed message is empty AND there are no attachments AND no context chips. Pressing Cmd/Ctrl+Enter inside the textarea (or clicking the send button) calls onSubmit with a ChatInputSubmitPayload:
type ChatInputSubmitPayload = {
message: string; // trimmed textarea value
attachments: Attachment[];
contexts: ContextChip[];
};Clear the textarea and attachments yourself in the submit handler — the composer does not reset state automatically.
The textarea exposes a default aria-label of "Message" (override via the aria-label prop on ChatInputTextarea or the high-level ChatInput). The send and attach buttons are icon-only and ship with aria-labels. Context chip remove buttons are labeled "Remove {label}". Disabled state is propagated from ChatInputRoot to the textarea, attach trigger, and send button.
ChatInput accepts everything ChatInputRoot does, plus placeholder and maxHeight which are forwarded to the internal ChatInputTextarea.
value?:
defaultValue?:
onValueChange?:
onSubmit?:
attachments?:
defaultAttachments?:
onAttachmentsChange?:
contexts?:
onContextRemove?:
placeholder?:
maxHeight?:
disabled?:
accept?:
maxFiles?:
maxSize?:
multiple?:
onFilesAccepted?:
onFilesRejected?:
className?:
Inherits all native <textarea> props except value, defaultValue, and onChange (managed by the root).
maxHeight?:
placeholder?:
aria-label?:
className?:
label
icon?:
onRemove?:
className?:
Inherits the FilePickerChatTrigger API. Each prop overrides the matching value inherited from ChatInputRoot.
accept?:
maxFiles?:
maxSize?:
multiple?:
onFilesAccepted?:
onFilesRejected?:
aria-label?:
Inherits the Button API except size, variant, asChild, type, and children, which are fixed by the component.
icon?:
aria-label?:
disabled?:
className?:
Standalone single-line variant. Does not require ChatInputRoot. Inherits all native <input> props except value, defaultValue, onChange, onSubmit, and ref.
value?:
defaultValue?:
onValueChange?:
onSubmit?:
placeholder?:
aria-label?:
aria-labelledby?:
hideSend?:
sendIcon?:
sendAriaLabel?:
disabled?:
ref?:
className?:
type Attachment = {
id: string;
src: string;
alt: string;
loading?: boolean;
};
type ContextChip = {
id: string;
label: string;
icon?: ReactNode;
};
type ChatInputSubmitPayload = {
message: string;
attachments: Attachment[];
contexts: ContextChip[];
};