Chat composer

Chat composer is the box people write messages in. It's a set of parts around one draft, not one component with a prop for every case: each chat surface picks the parts it needs, puts them in its own order, and adds its own elements beside them.

Chat composerType and press Enter to send. Shift+Enter breaks the line, and files dropped on it attach.

Examples

  • token-audit.pdf471 KB
  • button-states.png1.7 MB
With attachmentsFiles picked or dropped show as chips in the header, each with its size and a remove button.
MinimalA text box and a send button in one row. Leaving parts out is the whole configuration.
Disableddisabled on the provider turns off the text box and every action.

Edit message

Editing a sent message: a header says so, the footer swaps the actions for Cancel and Save, and Escape cancels through the text box's own onKeyDown. No isEditing prop.

Editing message
Esc to cancel
Save, then press Edit to bring it back.

Thread reply

A reply in a thread, with a checkbox to also post it to the channel. The checkbox is ordinary JSX in the footer, and onSubmit reads it.

Replying to Ada Lovelace
The checkbox sits between the actions and send.

Submit outside the frame

A forward dialog whose Forward button sits in the dialog's footer, outside the frame. It sends because it's inside the provider. The note is optional, so the provider's canSubmit lets an empty draft through.

Forward message
ToGHGrace Hopper
The tokens land on Friday.

Synced draft

Two composers on one store, as a draft synced between a laptop and a phone would be. Type in either one. The parts are the same; only the provider changed.

Laptop
Phone

In a chat

Under a message list, with a pretend round trip so the send button shows its busy state.

  1. AL
    Ada LovelaceIs the composer ready to try?
Send a reply. The button holds a busy state through the round trip.

Bring your own state

Every part talks to this interface and nothing else. Render ChatComposerProvider with your own state and actions and the same parts run on them:

type ChatComposerState = {
  value: string
  attachments: ChatComposerAttachment[]
  submitting?: boolean
  disabled?: boolean
  canSubmit?: boolean // defaults to having text or an attachment
}
 
type ChatComposerActions = {
  setValue: (value: string) => void
  addAttachments: (files: File[]) => void
  removeAttachment: (id: string) => void
  submit: () => void
}
 
type ChatComposerAttachment = {
  id: string
  name: string
  size?: number // bytes
  file?: File
}

Custom parts read the same context with useChatComposer(), which returns { state, actions, meta }, with the text box at meta.inputRef.

Anatomy

Seven parts make up a full composer. Each is its own component, so a design picks the ones it needs rather than switching features off. A provider holds the draft and renders no element; a drop zone can wrap the frame, with no look of its own until files are dragged over it.

  • token-audit.pdf471 KB
  • button-states.png1.7 MB
  1. Frame
  2. Header
  3. Text box
  4. Footer
  5. Attachments
  6. Send
  7. Actions
The partsA frame with a header of attachments, the text box, and a footer of actions and send.

Guidelines

  • 01Compose a variant from parts instead of adding a prop. An edit box, a thread reply and a forward dialog are three compositions of the same parts.
  • 02Keep state out of the parts. If the draft has to survive a reload or follow someone across devices, swap LocalChatComposerProvider for a ChatComposerProvider fed by your store.
  • 03Put the provider around everything that needs the draft. A send button in a dialog's footer, outside the frame, still works when it's inside the provider.
  • 04Leave ChatComposerCommonActions out when a composer needs a different set, and list the actions it does need.
  • 05Return a promise from onSubmit when sending takes a round trip. The send button shows it's busy, and a rejection keeps the draft.

Content

  • 01Name where the message goes in the placeholder: Message #design, Reply to Ada. Avoid generic text such as Type here.
  • 02Headers say what's happening in a few words, in sentence case: Editing message, Replying to Ada Lovelace.
  • 03Give send a word when it does something other than post a new message: Save, Forward. Keep the arrow for a plain send.
  • 04Keep file names whole in the data. The chip truncates them for display and screen readers still hear the full name.

Best practices

Editing message
DoCompose a variant: a header that says what's happening and the actions it needs.
<Composer isEditing isThread hideAttach />
Don'tDon't add a boolean for each case. Every flag doubles the states the part has to get right.
DoKeep the send button at the end of the footer.
Don'tDon't move it because you can. People look for it where they finish typing.

Accessibility

KeyDoes
↵DoesSends the draft
Shift ↵DoesBreaks the line
TabDoesMoves to the actions and the send button
  • 01The text box is named Message by default. Pass aria-label when the placeholder says something more specific, such as Edit message.
  • 02Icon buttons are named by their label, which is also their tooltip. The send button is named Send.
  • 03Enter that confirms a word in an input method editor never sends.
  • 04Each attachment has its own Remove button, named after the file.
  • 05Dropping files is a shortcut. The attach button does the same from the keyboard; give both the same accept.
  • 06The send button is aria-busy while a send is in flight, and its spinner holds still when reduced motion is on.
  • 07After a send, focus returns to the text box, so the next message starts where the last one ended.

References

  • Fernando Rojo, Composition is all you need, React Universe Conf 2025, the talk whose composer these parts follow
  • Base UI, for the button, tooltip and progress underneath