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.Root is a form. Enter in one of its textareas sends it through requestSubmit, 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. busy means a reply is running: no submit of this form goes through, from Enter, a submit button or requestSubmit, and the root sets data-busy.
  • Composer.Submit is a Button that sits at the end of its row. It is a submit button with the send glyph, named by the send string. While the composer is busy it is a plain button with the stop glyph, named by the stop string, that calls onStop. disabled applies to Send; Stop is disabled only when there is no onStop.

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

Composer.Root props
NameTypeDescription
onSubmit(event: React.SubmitEvent<HTMLFormElement>) => void

Called for this form's own submits, not those of a form rendered inside it with createPortal.

busyboolean

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

Composer.Submit props
NameTypeDescription
onStop() => void

Called when Stop is pressed. Without it, Stop is disabled while the composer is busy.

disabledboolean

Disables Send, such as while nothing is typed. Stop ignores it.

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