- 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
ChatMessage
A single message in a chat transcript — supports user bubbles, assistant prose, optional avatars, and an action bar for copy/regenerate/feedback affordances.
pnpm add @wandercom/design-system-web
import { ChatMessage } from '@wandercom/design-system-web/ui/chat-message';
export function Example() {
return <ChatMessage from="user">Hello</ChatMessage>;
}ChatMessage is a thin wrapper around the compound primitives. Reach for the primitives directly when you need fine-grained control.
The author is named from (not role) so the JSX prop doesn't collide with the HTML/ARIA role attribute.
The from prop drives alignment and visual treatment.
user— right-aligned bubble withsurface-secondarybackground,rounded-xlcorners, andbody-longtypography. Capped at ~36rem and constrained to leave room for the assistant on the opposite side.assistant— flush-left prose, no bubble, full transcript width. Pair with theMarkdownprimitive when rendering rich responses.
<ChatMessage from="user">Change the hero description.</ChatMessage>
<ChatMessage from="assistant">The hero description has been updated.</ChatMessage>For more control, compose the primitives directly:
import {
ChatMessageRoot,
ChatMessageAvatar,
ChatMessageContent,
ChatMessageMeta,
ChatMessageActions,
ChatMessageAction,
} from '@wandercom/design-system-web/ui/chat-message';
<ChatMessageRoot from="user">
<ChatMessageContent from="user">
Change background image to this.
<ChatMessageMeta>1 attachment</ChatMessageMeta>
</ChatMessageContent>
</ChatMessageRoot>;When the body is Markdown, opt in via markdown so ChatMessageContent skips its default Text wrapper and lets the Markdown primitive own block typography. ChatMessage does not import Markdown itself — wrap the children in your call site.
import { ChatMessage } from '@wandercom/design-system-web/ui/chat-message';
import { Markdown } from '@wandercom/design-system-web/ui/markdown';
<ChatMessage from="assistant" markdown>
<Markdown>{response}</Markdown>
</ChatMessage>;Mark the assistant's most recent streaming message as live so screen readers announce it once it settles. The message renders with aria-live="polite" and aria-atomic="false" — partial token updates queue without firing mid-stream announcements, and the final text is read once additions stop.
<ChatMessage from="assistant" live markdown>
<Markdown>{streamingContent}</Markdown>
</ChatMessage>Set live only on the message that is actively arriving. Setting it on every message defeats the point — screen readers will re-announce on every change. Once the message is stable, drop the prop or move it to the next arriving turn.
The parent ChatContainer deliberately does not declare itself a role="log" / aria-live region, because doing so would re-announce on every streamed token across the whole transcript.
Render a horizontal action bar below the message via the actions prop or compose ChatMessageActions and ChatMessageAction directly. Each action is an icon-only ghost button with a tooltip and matching aria-label.
import { IconClipboard } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconClipboard';
import { IconReplay } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconReplay';
<ChatMessage
from="assistant"
actions={[
{ label: 'Copy', icon: <IconClipboard />, onClick: copy },
{ label: 'Regenerate', icon: <IconReplay />, onClick: regen },
]}
>
The hero description has been updated.
</ChatMessage>;from
children
avatar?
actions?
markdown?
live?
contentClassName?
className?
from
live?
asChild?
className?
from
markdown?
asChild?
className?
Forwards through to the shared Avatar primitive with size="sm" as the default and shrink-0 applied. Accepts the full Avatar prop surface (src, alt, fallback, size, className).
children
className?
children
className?
label
icon
tooltip?
onClick?
- Every action is a real
<button>(via the sharedButtonprimitive) with botharia-labeland a tooltip set fromlabel. Never ship an action without a label. - Live-region semantics are scoped to individual arriving messages via the
liveprop, so streaming tokens don't fire announcements on every keystroke. The parentChatContainerdeliberately does not declare itself arole="log"/aria-liveregion.ChatThinkingcarries its ownaria-live="polite"for the awaiting state. - The
user/assistantdistinction is exposed asdata-fromon the root and content elements for downstream styling and testing.
Keyboard interaction:
Tab/Shift+Tab— Move focus through actions.Space/Enter— Activate the focused action.Escape— Dismisses any open tooltip.