Composer
Composer.Root is the prompt input of a chat: a <form> around a Field that holds a Field.Textarea. Composer.Submit is the button that sends it, or stops the reply while one is running. The rest comes from Field: buttons go in a Field.Row after the textarea, and attachments in a Field.Row before it.
On this page
<Composer.Root busy={replying} onSubmit={send}>
<Field.Root aria-label="Message">
<Field.Textarea value={value} onChange={(event) => setValue(event.target.value)} autoGrow maxRows={6} />
<Field.Row>
<Composer.Submit disabled={value.trim() === ''} onStop={stop} />
</Field.Row>
</Field.Root>
</Composer.Root>
Parts
Composer.Rootis aform. Enter in one of its textareas sends it throughrequestSubmit, with the form's first submit button as the submitter, and sends nothing while that button is disabled. Shift+Enter, Enter with Alt, Ctrl or Meta, and Enter that ends an IME composition, such as a Korean word, are left to the textarea.busymeans a reply is running: no submit of this form goes through, from Enter, a submit button orrequestSubmit, and the root setsdata-busy.Composer.Submitis a Button that sits at the end of its row. It is a submit button with thesendglyph, named by thesendstring. While the composer is busy it is a plain button with thestopglyph, named by thestopstring, that callsonStop.disabledapplies to Send; Stop is disabled only when there is noonStop.
Sending and stopping
Send from the root's onSubmit, calling event.preventDefault() to stay on the page, and set busy until the reply is done. onSubmit gets the composer's own submits, not those of a form in a portal inside it, rendered with createPortal. Send and Stop are the same button, so a button that has focus keeps it as they swap, while it stays enabled. Composer ignores the second click of a double click on the button, and the repeats of a held Enter on the button or in the textarea, so a second click meant as Stop right after Send does not stop. Your own onClick still runs for the second click. A click on Stop does not also send what was typed while the reply ran.
When Send turns disabled while it has focus and the browser takes focus off it, as Chromium does, such as when Stop turns back into Send with nothing typed, focus moves to the first enabled textarea of the form.
Attachments
Put each Attachment in a labelled list in a Field.Row before the textarea. To take pasted and dropped files, spread useDropzone's getRootProps() on Composer.Root with paste: true. A button in the bottom row can open the file picker through the input's ref, as the example above does.
Accessible composition
Todae handles: Enter to send, and Shift+Enter or an IME composition to keep writing. The names of Send and Stop, from StringsProvider. Focus staying on a focused button as Send and Stop swap, and moving to the text field when the browser takes focus off the button as it turns disabled.
You supply: A name for the field, such as aria-label on Field.Root or a Label. busy while the reply runs, and onStop. A name for every other button in the row. Announcing the reply, such as through Conversation.
Watch for: A placeholder as the field's only name. A file input hidden with no button that opens it, since dragging is pointer-only.
When to use
When: The text box where a person writes to an assistant or in a chat.
When not: A form with a text field and a button → Field and Button in a form. A single-line search → Field.TextInput.
Props
| Name | Type | Description |
|---|---|---|
onSubmit | (event: React.SubmitEvent<HTMLFormElement>) => void | Called for this form's own submits, not those of a form rendered inside it with createPortal. |
busy | boolean | A response is running: no submit of this form goes through, from Enter, a submit button or requestSubmit, and Composer.Submit turns into Stop. Sets data-busy. Default: false |
| Name | Type | Description |
|---|---|---|
onStop | () => void | Called when Stop is pressed. Without it, Stop is disabled while the composer is busy. |
disabled | boolean | Disables Send, such as while nothing is typed. Stop ignores it. |
children | React.ReactNode | The button's content. Without it, the send glyph named by the send string, or the stop glyph named by the stop string while busy. |