Skip to Content
⭐️ Leave a star →
ComponentsChat

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.
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

PropTypeDefaultDescription
valuestringControlled composer value
defaultValuestring''Initial composer value (uncontrolled)
onValueChange(value: string) => voidCalled when the composer text changes
onSubmit(value: string) => voidCalled on submit; the composer clears afterwards
isStreamingbooleanfalseWhen true, submission is blocked
disabledbooleanfalseDisable the composer

List props

PropTypeDefaultDescription
countnumberrequiredTotal number of messages
estimateItemHeightnumber72Estimated row height in px before measurement
overscannumber6Extra rows rendered above/below the viewport
stickToBottombooleantrueKeep the view pinned to the newest message
children(props: { index: number }) => ReactNoderequiredRender a single message by index

Message props

PropTypeDefaultDescription
role'user' | 'assistant' | 'system' | string'user'Who sent the message — surfaced as data-role
streamingbooleanfalseMark as actively streaming — surfaced as data-streaming

Input props

PropTypeDefaultDescription
submitOnEnterbooleantrueSubmit on Enter (Shift+Enter inserts a newline)
autoResizebooleantrueAuto-grow the textarea to fit its content

Send props

PropTypeDescription
disabledbooleanOverride the automatic disabled state (auto-disabled while streaming, disabled, or empty)
...React.ButtonHTMLAttributes<HTMLButtonElement>Standard button attributes

Data attributes

AttributeElementValues
data-roleMessage"user" / "assistant" / "system" / custom
data-streamingRoot, Message, SendPresent while streaming
data-chat-itemList rowsPresent (marker)
data-chat-list-sizerList sizerPresent (marker)

Accessibility

  • Chat.List sets role="log" with aria-live="polite" and aria-relevant="additions" so screen readers announce new messages.
  • Chat.Input reflects aria-disabled when the composer is disabled.
  • Chat.Send and the textarea are disabled appropriately to prevent submitting empty or in-flight messages.

Keyboard Interactions

KeyDescription
EnterSubmits the message (when submitOnEnter).
Shift+EnterInserts a newline instead of submitting.
Last updated on

MIT License © 2026 wire-ui

Chat – Wire UI