Chat - Headless composer
Assemble the draft surface from structural primitives that already handle submission, IME-safe Enter, attachments, helper text, and disabled states.
Primitive set
The composer surface is built from:
Composer.RootComposer.TextAreaComposer.ToolbarComposer.AttachButtonComposer.HelperTextComposer.SendButton
Canonical composition
import { Composer } from '@mui/x-chat/headless';
function ThreadComposer() {
return (
<Composer.Root>
<Composer.TextArea placeholder="Write a message" />
<Composer.HelperText />
<Composer.Toolbar>
<Composer.AttachButton />
<Composer.SendButton />
</Composer.Toolbar>
</Composer.Root>
);
}
This pattern gives you a working structural composer while leaving every visual decision in user land.
Composer.Root
Composer.Root is a structural form wrapper around the headless composer state.
It owns:
- submit-on-form-submit wiring
- composer context for the child primitives
- owner state such as
hasValue,isSubmitting,isStreaming, andattachmentCount
That makes it the place to style global draft states such as empty, busy, or attachment-heavy composers.
Composer.TextArea
Composer.TextArea is a textarea-based primitive that already handles the runtime-specific behaviors that usually make chat inputs fiddly to implement.
It supports:
- binding to the current composer value
- automatic textarea resizing as the draft grows
Enterto submitShift+Enterfor a new line- composition tracking for IME input
- focus restoration when the active conversation changes and the previous input unmounts
IME-safe Enter behavior
The input only submits when all of these are true:
- the key is
Enter Shiftis not pressed- the native event is not composing
- no earlier
onKeyDownhandler prevented the default behavior
That means East Asian IME flows stay intact without requiring extra app-level bookkeeping.
Input example
<Composer.TextArea aria-label="Message" minRows={1} placeholder="Reply in thread" />
If you replace the root slot, preserve the textarea-like behavior unless you are intentionally building a different draft surface.
Composer.AttachButton
Composer.AttachButton pairs a visible trigger with a hidden file input.
By default it:
- opens the hidden input on click
- accepts multiple files
- adds each selected file into the composer attachment collection
- resets the file input value after selection so the same file can be picked again
The primitive exposes both root and input slots, which is useful when you want a custom trigger element or need to style the hidden input for a testing harness.
Attachment example
<Composer.AttachButton
aria-label="Add files"
slotProps={{
input: {
accept: 'image/*,.pdf',
},
}}
/>
Composer.HelperText
Composer.HelperText is the default place for draft-level status and error messaging.
It renders:
- explicit children when you provide them
- otherwise the current runtime error message from the composer context
That makes it a good structural slot for validation copy, transport errors, and retry guidance.
<Composer.HelperText>
Files are uploaded after the message is sent.
</Composer.HelperText>
If you omit children, the component falls back to the active runtime error text and returns null when there is nothing to show.
Composer.SendButton
Composer.SendButton is a submit button that understands composer state.
It disables itself when:
- the draft is empty
- a stream is already active
- the button is disabled externally
The default button type is submit, so it works naturally inside Composer.Root.
Slots and owner state
Composer primitives expose slots and slotProps throughout the surface.
Custom slots receive owner state derived from the composer context, including:
hasValueisSubmittingisStreamingattachmentCount
Use these values for styling patterns such as:
- hiding the send button until a value exists
- emphasizing the attach trigger when attachments are present
- dimming the toolbar while a stream is active
See also
- Continue with Indicators to add typing, unread, and scroll affordances around the composer.
- Continue with Customization for slot and owner-state patterns across the full headless surface.
- Continue with Composer with attachments for the demo version of this page.
API
API
See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.