Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

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 Channel or a Thread. They read MessageInputContext and the composer from them, and throw outside both.
  • Keep the SDK's text input in a custom layout, by rendering InputView or AutoCompleteInput, 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. WithComponents reads 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, onSelectionChange and value replace the SDK's handling. With your own onChangeText, typing doesn't reach the composer.
  • placeholder replaces "Send a message", a command's placeholder, such as "Search GIFs" for /giphy, and the slow mode placeholder.
  • style replaces the SDK's style of the input, instead of adding to it.
  • numberOfLines sets how many lines the input grows to before it scrolls, 5 by 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:

  1. StopMessageStreamingButton, while the channel's AI state is thinking or generating. It calls Channel.stopAIResponse().
  2. The edit button, while a message is being edited or a command is active. It's disabled while the composition is empty.
  3. CooldownTimer, while cooldownIsActive is true.
  4. StartAudioRecordingButton, when audioRecordingEnabled is true and the composer has no text and no attachments.
  5. 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:

src/SendButtonWithCount.tsx
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