Composer UI components
The default MessageComposer is built from the parts on this page. Each part reads the composer from useMessageComposer(), so it works in a channel, in a thread and while a message is being edited. Message composer UI explains what each part shows and which store it reads.
Best practices
- Replace a part by registering it under its override key with
WithComponents, so the default layout keeps the rest. - Render the parts inside a
Channelor aThread. They readMessageInputContextand the composer from them, and throw outside both. - Keep the SDK's text input in a custom layout, by rendering
InputVieworAutoCompleteInput, so typing, mentions and commands keep reaching the composer. - To keep a default part and add to it, register a component that renders the exported part with your additions around it.
- Declare override objects once, at module scope.
WithComponentsreads them when it mounts.
Available building blocks
The parts below MessageComposer, in the order the row renders them:
| Component | Override key | What it renders | Props |
|---|---|---|---|
MessageComposerLeadingView |
MessageComposerLeadingView |
The area before the input box, with InputButtons. Hidden from the start of a recording until it's sent or deleted |
none |
InputButtons |
none | AttachButton, when a picker is available, the user has upload-file and no command is active |
Below |
AttachButton |
AttachButton |
The attach button, which opens or closes the attachment picker | Below |
MessageInputHeaderView |
MessageInputHeaderView |
The previews above the input: the message being edited or quoted, the attachments and the link previews. Hidden from the start of a recording until it's sent or deleted | none |
Reply |
Reply |
The edited message, with mode="edit", or the quoted message, with mode="reply", and a close button. Also quoted messages in message bubbles |
Message UI components |
AttachmentUploadPreviewList |
AttachmentUploadPreviewList |
The composer's attachments: audio files and voice recordings stacked, the others in a horizontal row | none |
ImageAttachmentUploadPreview |
ImageAttachmentUploadPreview |
An image, as a thumbnail with a remove button | Below |
| not exported | VideoAttachmentUploadPreview |
A video, as a thumbnail with its duration, or as a file row without a thumbnail | Below |
FileAttachmentUploadPreview |
FileAttachmentUploadPreview |
A file, as a row with its icon, name and size. Also audio files when no audio package is installed | Below |
AudioAttachmentUploadPreview |
AudioAttachmentUploadPreview |
An audio file or a voice recording, as a player | Below |
InputView |
InputView |
The command chip, AutoCompleteInput and ShowThreadMessageInChannelButton |
Below |
AutoCompleteInput |
none | The text input. It writes the text and the cursor into textComposer, which opens the suggestions |
Below |
ShowThreadMessageInChannelButton |
none | The "Also send to channel" checkbox, inside a Thread while allowThreadMessagesInChannel is true |
Below |
AutoCompleteSuggestionList |
AutoCompleteSuggestionList |
The suggestions for @, / and :. The message list renders it, above the composer |
none |
AutoCompleteSuggestionHeader |
AutoCompleteSuggestionHeader |
The list's header: "Instant Commands" for commands, and the emoji query for emoji. Nothing for mentions | Below |
AutoCompleteSuggestionItem |
AutoCompleteSuggestionItem |
One suggestion. Pressing it calls TextComposer.handleSelect() |
Below |
MentionSuggestionItem |
MentionSuggestionItem |
The content of an @ suggestion, by its mentionType: a user, @channel or @here, a role or a user group |
the MentionSuggestion's fields |
MessageInputTrailingView |
MessageInputTrailingView |
OutputButtons, unless a recording is locked or stopped |
none |
OutputButtons |
none | One control, the first that applies: the stop button, the edit button, the cooldown timer, the record button, the send button | Below |
StopMessageStreamingButton |
StopMessageStreamingButton |
The button that stops an AI reply, while the AI is thinking or generating | Below |
CooldownTimer |
CooldownTimer |
The seconds left of a slow mode cooldown, on a disabled button | none |
AudioRecordingButton |
StartAudioRecordingButton |
The record button | Audio recorder |
SendButton |
SendButton |
The send button | Below |
MessageComposerTrailingView |
MessageComposerTrailingView |
Nothing. Rendered after the input box, for a part of your own | none |
SendMessageDisallowedIndicator |
SendMessageDisallowedIndicator |
"You can't send messages in this channel", in place of the composer when the user lacks send-message |
none |
The edit button, which shows a checkmark while a message is being edited or a command is active, and the command chip have no override key and aren't exported. Replace MessageInputTrailingView to change the edit button, and InputView to change the command chip. LinkPreviewList isn't exported either: replace MessageInputHeaderView to change the link previews.
WithComponents also accepts the keys InputButtons, ShowThreadMessageInChannelButton, MessageInputLeadingView and MessageInputFooterView, but no component reads them, so registering them changes nothing. To change the parts they name, replace the part that renders them: MessageComposerLeadingView for InputButtons, and InputView for ShowThreadMessageInChannelButton. The exported MessageInputLeadingView and MessageInputFooterView render nothing, and the default layout doesn't render them.
The composer's recorder parts are on Audio recorder, and the attachment picker's on Attachment picker. The Input key replaces the whole row; see What it renders.
InputButtons props
InputButtons reads its props from MessageInputContext and the user's capabilities. Pass one to override it. It renders AttachButton when one of hasCameraPicker, hasFilePicker and hasImagePicker is true, uploadFile is true and no command is active. Otherwise it renders nothing.
| Prop | Type | Default | Description |
|---|---|---|---|
hasCameraPicker |
boolean |
from MessageInputContext |
Whether the camera picker is available. |
hasFilePicker |
boolean |
from MessageInputContext |
Whether the file picker is available. |
hasImagePicker |
boolean |
from MessageInputContext |
Whether the image picker is available. |
hasCommands |
boolean |
from MessageInputContext |
Whether the channel has commands. |
uploadFile |
boolean | undefined |
the upload-file capability |
Whether the user can upload files in the channel. |
AttachButton props
AttachButton reads its props from MessageInputContext and the attachment picker. Pass one to override it.
| Prop | Type | Default | Description |
|---|---|---|---|
handleOnPress |
((event: GestureResponderEvent) => void) & (() => void) |
none | Called on press, in place of handleAttachButtonPress and toggleAttachmentPicker. It's called without an event. |
handleAttachButtonPress |
() => void |
from MessageInputContext |
Called on press, in place of toggleAttachmentPicker, when handleOnPress isn't set. |
toggleAttachmentPicker |
() => void |
opens or closes the picker | Called on press otherwise. The default opens the picker, or, while it's open, focuses the input when focusInputOnPickerClose is true and closes the picker when it's false. |
isAttachmentPickerOpen |
boolean |
whether a picker source is selected, false |
Switches the button's accessibility label between adding an attachment and closing the attachments. |
disabled |
boolean |
false |
Disables the button. |
disableAttachmentPicker |
boolean |
from the attachment picker context | Not read by the button. |
InputView props
InputView passes its props to AutoCompleteInput, which renders the TextInput. MessageComposer passes TextInputComponent and the additionalTextInputProps it has. Both take every TextInput prop and these:
| Prop | Type | Default | Description |
|---|---|---|---|
TextInputComponent |
TextInputOverrideComponent |
React Native's TextInput |
The component that renders the input. It must forward its ref. |
cooldownRemainingSeconds |
number |
useCooldownRemaining() |
The seconds left of a slow mode cooldown. While it's set, the placeholder reads "Slow mode, wait 5s...". |
setInputBoxRef |
Ref<InputBoxRef> | undefined |
from MessageInputContext |
Receives the input, extended with clearState() and restoreState(text). |
channel |
Channel |
from ChannelContext |
Compared to skip re-renders; not passed to the TextInput. |
t |
StreamTFunction |
from TranslationContext |
Translates the placeholders. |
The TextInput props are spread after the SDK's own, so they replace them:
onChangeText,onSelectionChangeandvaluereplace the SDK's handling. With your ownonChangeText, typing doesn't reach the composer.placeholderreplaces "Send a message", a command's placeholder, such as "Search GIFs" for/giphy, and the slow mode placeholder.stylereplaces the SDK's style of the input, instead of adding to it.numberOfLinessets how many lines the input grows to before it scrolls,5by default.
The input is not editable while the composer's text.enabled setting is false, and its maxLength is the composer's text.maxLengthOnSend.
ShowThreadMessageInChannelButton props
ShowThreadMessageInChannelButton reads its props from ThreadContext. It renders only while thread is set and allowThreadMessagesInChannel is true. Pressing it toggles showReplyInChannel in the composer's state with MessageComposer.toggleShowReplyInChannel().
| Prop | Type | Default | Description |
|---|---|---|---|
thread |
Thread |
from ThreadContext |
The thread the composer is in. Without one, the checkbox doesn't show. |
allowThreadMessagesInChannel |
boolean |
from ThreadContext |
The Thread prop, true by default. false hides the checkbox. |
t |
StreamTFunction |
from TranslationContext |
Translates the label. |
Suggestion props
AutoCompleteSuggestionList takes no props. It reads textComposer.state.suggestions, renders the items of its search source, loads more of them at the end of the list, and announces the list to screen readers when it opens. It renders the two parts below with these props.
AutoCompleteSuggestionHeader:
| Prop | Type | Default | Description |
|---|---|---|---|
queryText |
string |
none | The text typed after the trigger. |
triggerType |
string |
none | "mention" for @, "command" for / and "emoji" for :. |
AutoCompleteSuggestionItem:
| Prop | Type | Default | Description |
|---|---|---|---|
itemProps |
TextComposerSuggestion |
required | The suggestion. Pressing the item passes it to TextComposer.handleSelect(). |
triggerType |
string |
none | Picks the content: MentionSuggestionItem for "mention", EmojiSuggestionItem for "emoji", CommandSuggestionItem for "command". |
MentionSuggestionItem gets the suggestion's fields as props. EmojiSuggestionItem and CommandSuggestionItem are exported too, and have no override key: replace AutoCompleteSuggestionItem to change them. Mentions and Suggestions explain where the suggestions come from.
Upload preview props
AttachmentUploadPreviewList takes no props. It renders a preview per attachment, by type, with these props. With audioRecordingSendOnComplete, voice recordings get no preview, because they're sent at once.
| Prop | Type | Default | Description |
|---|---|---|---|
attachment |
A |
required | The attachment, of the preview's type, with its localMetadata: id, file, uploadState. |
handleRetry |
(attachment: LocalUploadAttachment) => void | Promise<LocalUploadAttachment | undefined> |
required | Uploads the attachment again. The list passes the composer's AttachmentManager.uploadAttachment(). |
removeAttachments |
(ids: string[]) => void |
required | Removes attachments from the composer by their localMetadata.id. |
ImageAttachmentUploadPreviewProps, FileAttachmentUploadPreviewProps and AudioAttachmentUploadPreviewProps are exported, each with A set to the attachment types it previews. The default video preview renders the SDK's image preview, not your ImageAttachmentUploadPreview, so replace VideoAttachmentUploadPreview to change videos too.
Upload state indicators
The previews show an attachment's upload state with these parts, each an override key. Which one shows depends on the attachment's uploadState and on the composer's attachments.pendingUploadsEnabled setting, which is off by default and which Chat turns on with enableOfflineSupport:
uploadState |
With pending uploads on | With pending uploads off (the default) |
|---|---|---|
uploading |
none | the in-progress indicator |
failed |
none | the retry indicator |
blocked |
the not-supported indicator | the not-supported indicator |
With pending uploads on, the message is sent while its attachments upload, and the message list shows their progress instead; see Sending with pending uploads. An image or video preview also shows ImageLoadingIndicator while its thumbnail loads.
| Component | Props | What it renders |
|---|---|---|
FileUploadInProgressIndicator |
localId?: string, sourceUrl?: string, totalBytes?: number | string | null |
While client.uploadManager has the upload, the AttachmentUploadIndicator override, a progress ring or a spinner while the progress is unknown, and the megabytes sent of totalBytes. |
FileUploadRetryIndicator |
onPress: () => void |
"Network error" and a "Retry Upload" button that calls onPress. |
FileUploadNotSupportedIndicator |
localMetadata: LocalAttachmentUploadMetadata |
"File too large" when the upload check failed on the size limit, "Not supported" otherwise. |
ImageUploadInProgressIndicator |
localId?: string, sourceUrl?: string, totalBytes?: number | string | null |
The AttachmentUploadIndicator override over the thumbnail. totalBytes isn't read. |
ImageUploadRetryIndicator |
onRetryHandler: () => void |
A retry badge that calls onRetryHandler. |
ImageUploadNotSupportedIndicator |
none | An error mark in the thumbnail's corner. |
The file and image versions of the in-progress indicator take UploadInProgressIndicatorProps, and the others FileUploadRetryIndicatorProps, FileUploadNotSupportedIndicatorProps and ImageUploadRetryIndicatorProps.
AttachmentUploadIndicator draws the progress with two more override keys, which the attachments of a message sent with pending uploads use too. For a file, it renders CircularProgressIndicator, 16 points wide, while the progress is known, and an ActivityIndicator while it isn't. For an image or a video, it renders MediaUploadProgressOverlay, which covers the thumbnail and draws a CircularProgressIndicator, 28 points wide with a stroke of 3, or an ActivityIndicator, in its center.
CircularProgressIndicator takes CircularProgressIndicatorProps:
| Prop | Type | Default | Description |
|---|---|---|---|
progress |
number |
required | The progress as a percentage, clamped to 0 to 100. The ring animates to a new value. NaN draws a spinning arc instead of the ring. |
filledColor |
ColorValue |
required | The color of the part of the ring that's done. The SDK passes accentPrimary. |
unfilledColor |
ColorValue |
required | The color of the rest of the ring. The SDK passes borderCoreDefault. |
backgroundColor |
ColorValue |
required | The fill inside the ring. The SDK passes backgroundCoreElevation0. |
size |
number |
16 |
The width and height, in points. |
strokeWidth |
number |
2 |
The width of the ring. |
style |
StyleProp<ViewStyle> |
The style of the ring's view. The SDK passes messageItemView.attachmentUploadIndicator.indicator from the theme inside the overlay. |
|
testID |
string |
The test ID of the ring's view. |
MediaUploadProgressOverlay takes MediaUploadProgressOverlayProps:
| Prop | Type | Default | Description |
|---|---|---|---|
progress |
number |
The progress as a percentage. Without it, the overlay shows an ActivityIndicator. |
|
size |
number |
18 |
The size passed to CircularProgressIndicator. AttachmentUploadIndicator passes 28. |
strokeWidth |
number |
3 |
The stroke width passed to CircularProgressIndicator. |
testID |
string |
Gives the overlay's view this test ID with -content appended. |
The overlay's background is the backgroundCoreOverlayLight token, and messageItemView.attachmentUploadIndicator.overlayContent in the theme styles it. It doesn't take touches, so the thumbnail under it stays pressable.
OutputButtons props
OutputButtons reads its props from MessageInputContext, the channel and the composer. Pass one to override it. It renders, in this order, the first control that applies:
StopMessageStreamingButton, while the channel's AI state is thinking or generating. It callsChannel.stopAIResponse().- The edit button, while a message is being edited or a command is active. It's disabled while the composition is empty.
CooldownTimer, whilecooldownIsActiveistrue.StartAudioRecordingButton, whenaudioRecordingEnabledistrueand the composer has no text and no attachments.SendButton, otherwise. It's disabled until the composer has something to send.
| Prop | Type | Default | Description |
|---|---|---|---|
channel |
Channel |
from ChannelContext |
The channel whose AI state is read and whose reply is stopped. |
cooldownIsActive |
boolean |
whether a slow mode cooldown runs | Shows CooldownTimer. |
audioRecordingEnabled |
boolean |
from MessageInputContext |
Shows the record button while the composer is empty. |
hasAttachments |
boolean |
whether the composer has attachments | Hides the record button. |
isOnline |
boolean |
useSettledWSConnectionHealth() |
Disables the send button while a command is active and it's false. The edit button replaces the send button while a command is active, so this has no effect. |
asyncMessagesLockDistance |
number |
from MessageInputContext |
Not read. |
asyncMessagesMinimumPressDuration |
number |
from MessageInputContext |
Not read. |
asyncMessagesSlideToCancelDistance |
number |
from MessageInputContext |
Not read. |
audioRecordingSendOnComplete |
boolean |
from MessageInputContext |
Not read. |
SendButton props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled |
boolean |
required | Disables the button. OutputButtons always passes it. A replacement rendered without it is treated as enabled. |
sendMessage |
() => Promise<void> |
from MessageInputContext |
Called on press. The default sends the composition, or saves the edit; see MessageInputContext. |
StopMessageStreamingButton props
| Prop | Type | Default | Description |
|---|---|---|---|
onPress |
() => void |
none | Called on press. OutputButtons passes a function that calls Channel.stopAIResponse(). |
SDK integration explains the AI state.
Example
This send button keeps the default and adds a counter of the characters typed. It registers as SendButton, so it shows in the same cases as the default:
import { StyleSheet, Text, View } from "react-native";
import type { TextComposerState } from "stream-chat";
import type { SendButtonProps } from "stream-chat-react-native";
import {
SendButton,
useMessageComposer,
useStateStore,
WithComponents,
} from "stream-chat-react-native";
import type { PropsWithChildren } from "react";
const textLengthSelector = ({ text }: TextComposerState) => ({
length: text.length,
});
const SendButtonWithCount = (props: SendButtonProps) => {
const { textComposer } = useMessageComposer();
const { length } = useStateStore(textComposer.state, textLengthSelector);
return (
<View style={styles.row}>
{length ? <Text style={styles.count}>{length}</Text> : null}
<SendButton {...props} />
</View>
);
};
const overrides = { SendButton: SendButtonWithCount };
export const ComposerOverrides = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);
const styles = StyleSheet.create({
count: { fontSize: 12 },
row: { alignItems: "center", flexDirection: "row", gap: 4 },
});Wrap the Channel and Thread screens in ComposerOverrides. The Custom composer UI cookbook builds a whole composer row of its own.
Hooks
useMessageComposer: the composer the parts write into.useAttachmentManagerState: the attachments and whether more fit.useMessageComposerHasSendableData: whether the send button is enabled.usePendingUploadsEnabled: whether the composer sends before uploads finish.useCooldownRemaining: the seconds left of a slow mode cooldown.