Chat
Streaming-aware chat primitives: a virtualized Chat.List, role-tagged Chat.Message, and a Chat.Composer + Chat.Input + Chat.Send that share state through Chat.Root.
Features
Virtualized list — only the visible rows are mounted, scales to thousands of messages.
Sticks to the bottom as new tokens arrive (great for streaming).
Composer state (value, submit, streaming) is shared through Root.
Auto-growing textarea with submit-on-Enter (Shift+Enter for newline).
Submission is blocked while streaming or disabled.
Messages carry a data-role and data-streaming for styling.
Example
Hey! Ask me anything about Wire UI.
What makes the Chat component special?
It is headless and streaming-aware: the list is virtualized, messages carry a data-role, and it pins to the bottom as new tokens arrive.
Anatomy
import { Chat } from '@wire-ui/react'
<Chat.Root onSubmit={send}>
<Chat.List count={messages.length}>
{({ index }) => (
<Chat.Message role={messages[index].role}>
{messages[index].text}
</Chat.Message>
)}
</Chat.List>
<Chat.Composer>
<Chat.Input placeholder="Send a message…" />
<Chat.Send>Send</Chat.Send>
</Chat.Composer>
</Chat.Root>Styling
Chat is headless — every part is styled with className/class. The Root and Send set data-streaming while the assistant responds, and each Message exposes data-role and data-streaming so you can style user vs. assistant bubbles.
<Chat.Message role="user" className="
data-[role=user]:justify-end
data-[role=assistant]:justify-start
data-[streaming]:animate-pulse
" />Using data attributes
/* User vs assistant alignment */
[data-role="user"] { justify-content: flex-end; }
[data-role="assistant"] { justify-content: flex-start; }
/* Streaming state */
[data-streaming] .cursor { opacity: 1; }Root props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled composer value |
defaultValue | string | '' | Initial composer value (uncontrolled) |
onValueChange | (value: string) => void | — | Called when the composer text changes |
onSubmit | (value: string) => void | — | Called on submit; the composer clears afterwards |
isStreaming | boolean | false | When true, submission is blocked |
disabled | boolean | false | Disable the composer |
List props
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | required | Total number of messages |
estimateItemHeight | number | 72 | Estimated row height in px before measurement |
overscan | number | 6 | Extra rows rendered above/below the viewport |
stickToBottom | boolean | true | Keep the view pinned to the newest message |
children | (props: { index: number }) => ReactNode | required | Render a single message by index |
Message props
| Prop | Type | Default | Description |
|---|---|---|---|
role | 'user' | 'assistant' | 'system' | string | 'user' | Who sent the message — surfaced as data-role |
streaming | boolean | false | Mark as actively streaming — surfaced as data-streaming |
Input props
| Prop | Type | Default | Description |
|---|---|---|---|
submitOnEnter | boolean | true | Submit on Enter (Shift+Enter inserts a newline) |
autoResize | boolean | true | Auto-grow the textarea to fit its content |
Send props
| Prop | Type | Description |
|---|---|---|
disabled | boolean | Override the automatic disabled state (auto-disabled while streaming, disabled, or empty) |
...React.ButtonHTMLAttributes<HTMLButtonElement> | — | Standard button attributes |
Data attributes
| Attribute | Element | Values |
|---|---|---|
data-role | Message | "user" / "assistant" / "system" / custom |
data-streaming | Root, Message, Send | Present while streaming |
data-chat-item | List rows | Present (marker) |
data-chat-list-sizer | List sizer | Present (marker) |
Accessibility
Chat.Listsetsrole="log"witharia-live="polite"andaria-relevant="additions"so screen readers announce new messages.Chat.Inputreflectsaria-disabledwhen the composer is disabled.Chat.Sendand the textarea are disabled appropriately to prevent submitting empty or in-flight messages.
Keyboard Interactions
| Key | Description |
|---|---|
| Enter | Submits the message (when submitOnEnter). |
| Shift+Enter | Inserts a newline instead of submitting. |
Last updated on