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.
Examples
- token-audit.pdf471 KB
- button-states.png1.7 MB
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.
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.
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.
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.
In a chat
Under a message list, with a pretend round trip so the send button shows its busy state.
- ALAda LovelaceIs the composer ready to try?
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
- Frame
- Header
- Text box
- Footer
- Attachments
- Send
- Actions
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
LocalChatComposerProviderfor aChatComposerProviderfed 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
ChatComposerCommonActionsout when a composer needs a different set, and list the actions it does need. - 05Return a promise from
onSubmitwhen 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
<Composer isEditing isThread hideAttach />Accessibility
| Key | Does |
|---|---|
| ↵ | DoesSends the draft |
| Shift ↵ | DoesBreaks the line |
| Tab | DoesMoves to the actions and the send button |
- 01The text box is named Message by default. Pass
aria-labelwhen 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.
Related components
- Typing indicatorSays who's typing in a conversation, naming up to three people.
- ButtonIn fibo's Storybook
- Filter menuA filter menu of fields and values that turns into a search as you type.
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