Composer
The field where a message is written, under a MessageList: attachments, a text field that grows with its text, your tools and a send button. Enter sends and Shift+Enter starts a new line. While a reply is on its way the send button can become a stop button.
Preview
import { IconPaperclip } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Composer } from "@/components/ui/composer"
const sendMessage = (text: string) => { /* Add the message to the conversation. */ }
<Composer label="Message" placeholder="Write a message" onSubmit={sendMessage} actions={<Button size="sm" variant="ghost" iconOnly aria-label="Attach a file">
<IconPaperclip size={16} />
</Button>} className="w-full max-w-md" />Usage
When to use
- Write and send a message under a conversation, with attachments and a few tools.
- Ask an assistant something, and stop its reply while it streams in.
When not to use
- A form field that is submitted with other fields. Use Textarea in a Field.
- Search a list or the app. Use FilterBar or Command.
- A one-line value. Use Input.
Examples
Message
import { IconPaperclip } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Composer } from "@/components/ui/composer"
const sendMessage = (text: string) => { /* Add the message to the conversation. */ }
<Composer label="Message" placeholder="Write a message" onSubmit={sendMessage} actions={<Button size="sm" variant="ghost" iconOnly aria-label="Attach a file">
<IconPaperclip size={16} />
</Button>} className="max-w-md" />With attachments
Files show above the text while they upload; canSubmit lets the message go without text.
import { IconPaperclip } from "@tabler/icons-react"
import { AttachmentPreview } from "@/components/ui/attachment-preview"
import { Button } from "@/components/ui/button"
import { Composer } from "@/components/ui/composer"
const sendMessage = (text: string) => { /* Add the message to the conversation. */ }
const removeFile = () => { /* Remove the file from the message. */ }
<Composer canSubmit label="Message" placeholder="Add a message" onSubmit={sendMessage} attachments={<AttachmentPreview name="brief.pdf" type="application/pdf" fileSize={482000} onRemove={removeFile} />} actions={<Button size="sm" variant="ghost" iconOnly aria-label="Attach a file">
<IconPaperclip size={16} />
</Button>} className="max-w-md" />Stop a reply
While the assistant answers, the send button becomes a stop button.
import { Composer } from "@/components/ui/composer"
const sendMessage = (text: string) => { /* Add the message to the conversation. */ }
const stopReply = () => { /* Cancel the reply that is streaming in. */ }
<Composer pending label="Message" placeholder="Ask anything" onSubmit={sendMessage} onStop={stopReply} className="max-w-md" />Reply
Reply in MessageActions puts the message in replyTo; the bar sits above the text until it is sent or canceled.
import { Composer } from "@/components/ui/composer"
const sendMessage = (text: string) => { /* Add the message to the conversation. */ }
const cancelReply = () => { /* Clear the Composer's replyTo. */ }
const sampleReply = { author: "Linh Tran", text: "Can you send the brief before the call?" }
<Composer label="Message" placeholder="Write a reply" replyTo={sampleReply} onCancelReply={cancelReply} onSubmit={sendMessage} className="max-w-md" />Tell others you are typing
onTypingChange reports when typing starts and stops; send it over your realtime channel.
import { Composer } from "@/components/ui/composer"
const sendMessage = (text: string) => { /* Add the message to the conversation. */ }
const sendTyping = (typing: boolean) => { /* Tell the others in the conversation, such as over a websocket. */ }
<Composer label="Message" placeholder="Write a message" onTypingChange={sendTyping} onSubmit={sendMessage} className="max-w-md" />Do and don't
Do
- Place the Composer under its MessageList, and keep focus in it after sending.
- Turn pending on while an assistant's reply streams in, and pass onStop so the reader can cancel it.
- Put only a few tools in actions, such as attach and a model or format picker.
Don't
- Do not clear a controlled composer before onSubmit has the text; clear it in onSubmit.
- Do not use the placeholder as the name; label names the field.
- Do not add a second send button outside the field.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| label required | string | - | - | Names the text field for screen readers, such as Message. |
| placeholder | string | - | - | A hint inside the empty field, such as Write a message. |
| value | string | - | - | The text, for a controlled composer. Clear it yourself in onSubmit. |
| defaultValue | string | - | - | The text at first render, for an uncontrolled composer. It clears itself after sending. |
| onValueChange | (value: string) => void | - | - | Called with the text on every change. |
| onSubmit | (value: string) => void | - | - | Called with the text when the message is sent. |
| canSubmit | boolean | - | - | Whether the message can be sent. Default: the text is not blank. Pass true to send attachments without text. |
| pending | boolean | - | false | A reply is on its way: nothing can be sent, and with onStop the send button becomes a stop button. Typing still works. |
| onStop | () => void | - | - | Called by the stop button while pending, such as to cancel a reply that streams in. |
| disabled | boolean | - | false | Nothing can be typed or sent, such as in a conversation the reader has left. |
| maxRows | number | - | 8 | The text field grows with its text up to this many lines, then scrolls. |
| submitOnEnter | boolean | - | true | Enter sends and Shift+Enter starts a new line. With false, Enter starts a new line and only the button sends. |
| actions | node | - | - | Tools at the start of the toolbar, such as an attach button. Use ghost icon Buttons in size sm. |
| attachments | node | - | - | AttachmentPreview tiles above the text. |
| replyTo | { author: React.ReactNode; text: React.ReactNode } | - | - | The message being answered, shown in a bar above the text and read with the text field. |
| onCancelReply | () => void | - | - | Shows a button in the reply bar that cancels the reply. |
| onTypingChange | (typing: boolean) => void | - | - | Called with true when the reader starts typing and false once they stop: after typingTimeout without a key, when the text is cleared or sent, when the field loses focus, and on unmount. Send it to the others, who show it with MessageList typingUsers. |
| typingTimeout | number | - | 3000 | How long without typing, in ms, before onTypingChange(false). |
| name | string | - | - | The name of the text field in a submitted form. |
| autoFocus | boolean | - | false | Focuses the text field on mount, such as when a conversation opens. |
| labels | { send?: string; stop?: string; replyingTo?: string; cancelReply?: string } | - | - | Button names and the words before a reply. Defaults: Send, Stop, Replying to, Cancel reply. Vietnamese: Gửi, Dừng, Đang trả lời, Hủy trả lời. |
Anatomy
| Part | Description |
|---|---|
| root | The form, drawn as Textarea's raised field box: attachments, text and toolbar stacked. |
| reply | The bar of the message being answered, first in the box: a reply icon, the author and one line of the message, and a cancel button. |
| attachments | Optional row of AttachmentPreview tiles above the text. It wraps. |
| input | The text field. One line at first; it grows with its text up to maxRows lines, then scrolls. |
| placeholder | The hint in the empty field, in secondary text. |
| toolbar | The row under the text: your tools at the start, the send or stop button at the end. |
| actions | Your tools, such as an attach button. Ghost icon Buttons in size sm. |
| send | The solid icon Button in size sm that sends, or the secondary stop button while pending. |
Accessibility
Role: textbox
| Keys | Action |
|---|---|
| Enter | Sends the message, unless submitOnEnter is false. Enter that confirms an input method's text never sends. |
| Shift + Enter | Starts a new line. |
| Tab | Moves from the text field to the attachments, the tools and the send button. |
- A form with a multi-line text field named by label (EN `Message`, VI `Tin nhắn`). The placeholder is a hint, not the name.
- The send button is named by labels.send (EN `Send`, VI `Gửi`). With nothing to send it is aria-disabled: it keeps its place in the tab order and does nothing.
- While pending with onStop, the send button becomes a stop button named by labels.stop (EN `Stop`, VI `Dừng`).
- Sending keeps focus in the text field, so the next message can be typed at once.
- The field box shows the focus halo only while the text field has focus; a focused tool keeps just its own ring.
- On a phone the Enter key shows Send (enterkeyhint).
- While replying, the text field is described by the reply bar, so a screen reader hears EN `Replying to Linh Tran` with the message.
- onTypingChange fires once when typing starts and once when it stops, never on every key, so the events it sends stay few.
Install
npx werewolf-ui add composerRun npx werewolf-ui init once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See Installation for requirements and the alternative registry setup.