Message composer UI

The message composer UI is where a user writes, edits and sends a message. The message composition is held by a MessageComposer controller from stream-chat, in its state stores: the text, attachments, quoted message, poll and location. The UI components subscribe to that state and call the controller's API to carry out what the user does. Use this page to know which MessageComposer controller the UI uses (the channel's, the thread's, or one you supply), what each part of the UI shows, and how to add or replace parts. The props are in the MessageComposer reference, and the controller is explained on Message composer.

The default message composer with its parts outlined and numbered: 1, a quoted message preview; 2, an image attachment preview; 3, a link preview; 4, the text input; 5, the attachment selector button; 6, the emoji picker button; 7, the send button

This composer is replying to Cara, with an image attached and a link in its text. Its parts, from top to bottom:

Part Component to override (<WithComponents/> key) Explained in
1. The quoted message preview QuotedMessagePreview Previews
2. The attachment previews AttachmentPreviewList Previews
3. The link preview LinkPreviewList Previews
4. The text input TextareaComposer The input
5. The attachment selector AttachmentSelector Actions
6. The emoji picker EmojiPicker, in AdditionalMessageComposerActions Actions
7. The send button SendButton Actions

Which composer it drives

<MessageComposer/>, its parts and your own components get the composer from useMessageComposerController(). It returns a composer supplied with MessageComposerControllerProvider, or else the thread's or the channel's:

graph LR
    H["useMessageComposerController()"] --> P{"Supplied by a provider?"}
    P -- no --> T{"In a Thread?"}
    P -- yes --> S["supplied composer"]
    T -- yes --> TC["thread.messageComposer"]
    T -- no --> CC["channel.messageComposer"]

Supply a composer of your own for a separate composition, such as editing a message inline, so its state doesn't leak into the channel's or thread's composer already rendered in the UI. This InlineEditor component edits a message in a composer of its own:

import { useState } from "react";
import { MessageComposer as MessageComposerController } from "stream-chat";
import type { LocalMessage } from "stream-chat";
import {
  MessageComposer,
  MessageComposerControllerProvider,
  useChatContext,
} from "stream-chat-react";

export const InlineEditor = ({ message }: { message: LocalMessage }) => {
  const { client } = useChatContext();
  const [composer] = useState(
    () =>
      new MessageComposerController({
        client,
        composition: message,
        compositionContext: message,
      }),
  );

  return (
    <MessageComposerControllerProvider messageComposerController={composer}>
      <MessageComposer />
    </MessageComposerControllerProvider>
  );
};

Inline message editing builds the full editor.

The default "Edit" action edits in the channel's or thread's composer itself:

The user The composer
Picks "Edit" on a message keeps a copy of what the user was writing, loads the message, sets editedMessage
Sends saves the edit and discards the copy
Clicks ✕ on the edited-message preview cancels the edit and brings the copy back

When <MessageComposer/> unmounts:

The thread's or channel's composer A composer you supplied
Saved as a draft if drafts are on and no edit is in progress the same
Cleared yes no: you clear it

Previews

Above the input, the composer shows what the message will carry: the message being replied to, the files, a link card and so on. <MessageComposerPreviews/> renders these previews from a preview set.

The preview set

A preview set is an array, and each item in it describes one preview:

Field What it is
type The preview's name, unique within the set, such as "quotedMessage". Code that changes a set finds an item by it.
Component The component that draws the preview. It gets no props: it reads what it shows from the composer with useMessageComposerController().
isVisible A function that gets the composer's state and returns whether the preview shows. The state holds the attachments, link previews, location, quoted message, the message being edited, the poll id and the composer's custom data.

<MessageComposerPreviews/> goes through the set in order and renders the Component of every item whose isVisible returns true. When none does, it renders nothing, not even its wrapper. When any does, the wrapper renders, even if that item's Component then renders nothing, so an item that often has nothing to show should hide itself through isVisible.

Info:

Example

The quotedMessage item of the default set has the type "quotedMessage", a Component that renders the QuotedMessagePreview override, <QuotedMessagePreview/> by default, and an isVisible that returns true while a message is quoted and none is being edited:

  1. The user picks Reply on a message, and the composer now holds a quoted message in messageComposer.state.quotedMessage. The item's isVisible finds it, and the quoted message shows above the input.
  2. The user removes the quote with ✕, and the composer no longer holds one. isVisible finds none, and the preview goes away. No other item is visible, so the preview area goes away too.

The default set, defaultMessageComposerPreviewSet, has these items, in this order:

type Shows Visible while Looks like
editedMessage The message being edited, with ✕ to cancel the edit a message is being edited
Editing a message: the edited message preview titled Edit Message above the input, with a close button, and the send button showing a checkmark
quotedMessage The message being replied to, with ✕ to remove it a message is quoted and none is being edited
Replying to Bob: the quoted message preview titled Reply to Bob above the input, with a close button
voiceRecording A recording waiting to be sent the composer holds a voice recording
A voice recording waiting to be sent: a card with a play button, Voice message, 46.9 kB, a playback speed button and a close button
attachments Files, images and videos, with their upload progress the composer holds an attachment other than a voice recording
Attachment previews above the input: an uploaded image, and the file header-layout.pdf still uploading, at 553 kB of 1.20 MB
linkPreviews The first link's card. The displayLinkCount prop of <LinkPreviewList/> shows more. a link preview is loading or loaded
A link preview while it loads: a compact chip with a link icon and the URL getstream.io
A link preview card for getstream.io, with its title, description and URL, above the text that holds the link
location A location to share, as <GeolocationPreview/>. It can't be removed while editing. the composer holds a location
A location preview: Current location, with its coordinates, above the input

No item shows a poll: the default poll creation dialog sends the poll at once, so the composer doesn't keep one. Poll with text and attachments keeps the poll in the composer and adds an item that shows it.

Custom previews

To hide, reorder or add previews, build your own set from the default one, render <MessageComposerPreviews/> with it, and register that component under the MessageComposerPreviews key. The state isVisible gets is what useMessageComposerPreviewState returns.

Here is a ComposerPreviews component that hides the link cards and adds a hint while the user replies:

src/ComposerPreviews.tsx
import type { ReactNode } from "react";
import {
  defaultMessageComposerPreviewSet,
  MessageComposerPreviews,
  type MessageComposerPreviewSetItem,
  WithComponents,
} from "stream-chat-react";

// Your own preview: static text, so it reads no composer state.
const ReplyHint = () => (
  <div className="reply-hint">Everyone in the channel sees your reply.</div>
);

// The default previews without the link cards, then the hint while a message is quoted.
const previewSet: MessageComposerPreviewSetItem[] = [
  ...defaultMessageComposerPreviewSet.filter(
    ({ type }) => type !== "linkPreviews",
  ),
  {
    Component: ReplyHint,
    isVisible: ({ editedMessage, quotedMessage }) =>
      !editedMessage && !!quotedMessage,
    type: "replyHint",
  },
];

const CustomPreviews = () => (
  <MessageComposerPreviews messageComposerPreviewSet={previewSet} />
);

const overrides = { MessageComposerPreviews: CustomPreviews };

export const ComposerPreviews = ({ children }: { children: ReactNode }) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Hiding an item hides only its preview. The message still carries what the composer holds: here, the link previews still go out with it.

To build a preview area of your own, use the two hooks <MessageComposerPreviews/> is built on. useMessageComposerPreviewSetFilter returns the visible items of a set. useMessageComposerPreviewState returns the state that isVisible gets.

The input

<TextareaComposer/> is the text input. It reads and writes messageComposer.textComposer state: the text, the cursor position, the mentions and the active command.

Typing a trigger opens the suggestion list, <SuggestionList/>: @ suggests people and groups to mention, / suggests commands, and : suggests emoji once you add the emoji middleware from stream-chat-react/emojis to the text composer. Each item is a <SuggestionListItem/>. The suggestions come from the text composer's middleware; Suggestions explains them.

Picking a command, such as /giphy, shows it as a chip before the input, <CommandChip/>. The chip's ✕ clears the command.

The suggestion list open above the input after typing @ca, listing Cara
The /giphy command chip before the typed text cats, with a close button

Actions

The composer has three action areas around the input:

Area Where Shows
<AttachmentSelector/> before the input A menu to upload a file, create a poll, share a location (not in a thread) or pick a command. In a composer inside a message, such as an inline edit, only an upload button. It's an action set.
<AdditionalMessageComposerActions/> after the input Action buttons. The default shows only the emoji picker, when you register one under the EmojiPicker key, and hides it during a cooldown and while a command is active. Replace it to add buttons of your own, as in the ComposerWithCount example in What each part reads or in GIF picker.
The send area at the end The stop AI streaming button, the cooldown timer, the record button or the send button

The send area shows the first of these that applies:

Control Shown when See
<StopAIGenerationButton/>, which calls Channel.stopAIResponse() an AI is thinking or generating a reply in the channel. Register StopAIGenerationButton: null to turn it off. SDK integration
<CooldownTimer/>, the seconds left slow mode stops the user from sending Slow mode
<StartRecordingAudioButton/> the composer is empty, nothing is edited, no command is active, <MessageComposer audioRecordingEnabled /> is set and the browser can record. It's disabled while the composer accepts no uploads. While recording, <AudioRecorder/> replaces the whole composer. Voice recordings
<SendButton/> otherwise. Disabled until there's something to send; the default shows a checkmark while editing or with a command.
The cooldown timer showing 17 seconds in the send button's place, with the input reading Slow mode, wait 17s
An empty composer with voice recording on, showing the record button in the send button's place

In threads

In a thread, the composer shows a checkbox, <SendToChannelCheckbox/>, that also posts the reply to the channel. It reads "Also send in channel", or "Also send as a direct message" in a channel with two members. It toggles showReplyInChannel in the composer's state, and is hidden while editing.

A thread's composer with the Also send in channel checkbox under the input

Drag and drop

Files dropped on the composer are uploaded into it. The composer UI wraps itself in <WithDragAndDropUpload/>, which shows "Drag your files here" while files are dragged over it, and refuses the files that messageComposer.attachments.acceptedFiles in client.config doesn't accept.

A file dragged over the composer: an overlay reading Drag your files here covers it

To accept drops on a larger area, such as the whole channel, wrap that area in <WithDragAndDropUpload/>. Files dropped anywhere in it go to the composers inside. Here is a ChannelWithDropZone component that accepts drops on the whole channel:

src/ChannelWithDropZone.tsx
import {
  Channel,
  ChannelHeader,
  MessageComposer,
  MessageList,
  WithDragAndDropUpload,
} from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";

export const ChannelWithDropZone = ({
  channel,
}: {
  channel: StreamChannel;
}) => (
  <Channel channel={channel}>
    <WithDragAndDropUpload className="channel-drop-zone" component="section">
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </WithDragAndDropUpload>
  </Channel>
);

A thread works the same way: wrap its list and composer inside <Thread/>. The thread renders beside the channel, not inside it, so each wrapper uploads only to its own composer. A wrapper uploads a dropped file to every composer inside it, so wrap the channel and the thread separately. When wrappers are nested, only the outermost one handles drops. It gets the class str-chat__dropzone-root, which makes the overlay cover it; add your own layout with className if necessary.

The composer's own drop zone turns off during a cooldown and when the user can't upload files. A wrapper around it takes over and stays on, but the composer still refuses those uploads. The props are in the WithDragAndDropUpload reference.

The composer state each UI part reads

Each part of the composer UI subscribes to the state of the MessageComposer member it shows, and calls that member's API when the user acts:

MessageComposer member Used by
state The quoted and edited message previews, the input, the send button, the also-send-to-channel checkbox
textComposer The input, the suggestion list, the command chip
attachmentManager The attachment and voice recording previews, the input, drag and drop, the attachment selector
linkPreviewsManager The link previews
pollComposer The poll creation dialog
locationComposer The location preview and the share location dialog
customDataManager No built-in part. It holds custom fields that your own parts add to the message.

A UI part of your own reads the same way: get the composer with useMessageComposerController() and subscribe to a store with useStateStore. Here is a ComposerWithCount component that shows the number of characters typed, beside the emoji picker:

src/ComposerWithCount.tsx
import type { TextComposerState } from "stream-chat";
import {
  AdditionalMessageComposerActions,
  MessageComposer,
  useMessageComposerController,
  useStateStore,
  WithComponents,
} from "stream-chat-react";

const textSelector = ({ text }: TextComposerState) => ({ text });

const CharacterCount = () => {
  const { textComposer } = useMessageComposerController();
  const { text } = useStateStore(textComposer.state, textSelector);

  return <span className="character-count">{text.length}</span>;
};

// Renders the default part too, so the emoji picker stays.
const ActionsWithCount = () => (
  <>
    <CharacterCount />
    <AdditionalMessageComposerActions />
  </>
);

const overrides = { AdditionalMessageComposerActions: ActionsWithCount };

export const ComposerWithCount = () => (
  <WithComponents overrides={overrides}>
    <MessageComposer />
  </WithComponents>
);

The same part works in a channel, in a thread and while editing, because the hook returns the composer of the place it is rendered in.

Replacing parts

Register replacements with <WithComponents/>. Overriding components explains how overrides apply.

Component to override (<WithComponents/> key) Replaces
MessageComposerUI The whole composer UI. <MessageComposer/> still provides the composer.
MessageComposerPreviews The preview area. Render <MessageComposerPreviews/> with your own set, as in Custom previews.
EditedMessagePreview, QuotedMessagePreview The edited and quoted message previews
AttachmentPreviewList, VoiceRecordingPreviewSlot The attachment and voice recording previews
LinkPreviewList The link previews
TextareaComposer The input
AutocompleteSuggestionList, AutocompleteSuggestionItem The suggestion list and each suggestion
CommandChip The command chip
AttachmentSelector The attachment selector
AttachmentSelectorInitiationButtonContents The contents of the button that opens the selector
PollCreationDialog, ShareLocationDialog The dialogs the selector opens
PollCreationDialogControls The footer of the poll creation dialog: Cancel, and Send Poll, which creates the poll and sends it at once
AdditionalMessageComposerActions, EmojiPicker The action buttons after the input, and the emoji picker among them
SendButton The send button. Gets sendMessage, and no checkmark: a replacement shows its own.
CooldownTimer The cooldown timer
StartRecordingAudioButton, AudioRecorder The record button and the recorder
StopAIGenerationButton The stop button. null turns it off.
SendToChannelCheckbox The also-send-to-channel checkbox

A replacement for EditedMessagePreview, QuotedMessagePreview, VoiceRecordingPreviewSlot, AttachmentPreviewList or LinkPreviewList shows only while its item in the preview set is visible. Say your AttachmentPreviewList also draws the location. It still shows only while the composer holds attachments, so a location on its own wouldn't appear. Build your own preview set from defaultMessageComposerPreviewSet, as Custom previews shows, and in it:

  • make the attachments item's isVisible also return true while the composer holds a location;
  • leave out the location item, so the location isn't drawn twice.

The defaults of SendToChannelCheckbox and StopAIGenerationButton aren't exported, so a replacement for one of them can't render the default inside it.

The Custom composer recipe builds a composer UI of its own.

Used in