Skip to content

Pro pre-sale is open. Installation opens January 2027. See Pro

Components

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.

  • npm: 1 direct, 4 total
  • registry: 3 direct, 3 total

Preview

Props

  • label
  • placeholder
  • pending
  • disabled
  • maxRows
  • typingTimeout
  • tools
  • attachment
  • reply
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.

brief.pdf482 KB
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.

Replying to Linh TranCan you send the brief before the call?
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

NameTypeValuesDefaultDescription
label requiredstring--Names the text field for screen readers, such as Message.
placeholderstring--A hint inside the empty field, such as Write a message.
valuestring--The text, for a controlled composer. Clear it yourself in onSubmit.
defaultValuestring--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.
canSubmitboolean--Whether the message can be sent. Default: the text is not blank. Pass true to send attachments without text.
pendingboolean-falseA 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.
disabledboolean-falseNothing can be typed or sent, such as in a conversation the reader has left.
maxRowsnumber-8The text field grows with its text up to this many lines, then scrolls.
submitOnEnterboolean-trueEnter sends and Shift+Enter starts a new line. With false, Enter starts a new line and only the button sends.
actionsnode--Tools at the start of the toolbar, such as an attach button. Use ghost icon Buttons in size sm.
attachmentsnode--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.
typingTimeoutnumber-3000How long without typing, in ms, before onTypingChange(false).
namestring--The name of the text field in a submitted form.
autoFocusboolean-falseFocuses 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

PartDescription
rootThe form, drawn as Textarea's raised field box: attachments, text and toolbar stacked.
replyThe 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.
attachmentsOptional row of AttachmentPreview tiles above the text. It wraps.
inputThe text field. One line at first; it grows with its text up to maxRows lines, then scrolls.
placeholderThe hint in the empty field, in secondary text.
toolbarThe row under the text: your tools at the start, the send or stop button at the end.
actionsYour tools, such as an attach button. Ghost icon Buttons in size sm.
sendThe solid icon Button in size sm that sends, or the secondary stop button while pending.

Accessibility

Role: textbox

KeysAction
EnterSends the message, unless submitOnEnter is false. Enter that confirms an input method's text never sends.
Shift + EnterStarts a new line.
TabMoves 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 composer

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