# Custom composer

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

This recipe builds a composer with a layout of its own: the attachment button on the left, the previews and the input in the middle, and the send button on the right. It replaces only the layout. The composer object, sending, uploads, suggestions, commands and drafts keep working as before. Which parts the default composer has and what each one reads is explained in [Message composer UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/message-composer-ui/).

## Best practices

- Replace `MessageComposerUI` and keep rendering `MessageComposer`. `MessageComposer` picks the channel's or the thread's composer, saves the draft when it unmounts, and provides the context the parts need: the textarea, paste handling and voice recording.
- Build the layout from the exported parts. Each one reads the composer from `useMessageComposerController()`, so the same layout works in a channel and in a thread.
- Keep `TextareaComposer`. It sends typing events, opens the suggestion list for `@` and `/`, shows commands and sends on Enter.
- Render `MessageComposerActions` for the send button. It shows the cooldown timer during slow mode, the record button when the composer is empty, and the stop button while an AI is generating. A send button of your own calls the function from `useMessageComposerSubmitFn()`, which saves an edit or sends a new message.
- Hide the attachment button while a command is active, as the default composer does. A command takes the whole composer.
- Test the layout with a quoted message, attachments, a link, a voice recording, a command, in a thread and while editing.

## Build the layout

Three defaults aren't exported: the edited-message preview, the "also send in channel" checkbox and the location preview. The layout below works around them:

- While a message is being edited, it renders the default `MessageComposerUI`, which shows the message being edited and puts back what the user was writing when the edit is cancelled.
- It leaves Location out of the attachment menu, because a location attached from the menu would have no preview here.
- The checkbox for thread replies is the last part of this page.

```tsx title="src/CustomMessageComposer.tsx"
import type { MessageComposerState, TextComposerState } from "stream-chat";
import {
  AttachmentPreviewList,
  AttachmentSelector,
  AudioRecorder,
  CommandChip,
  defaultAttachmentSelectorActionSet,
  LinkPreviewList,
  MessageComposerActions,
  MessageComposerUI,
  QuotedMessagePreview,
  TextareaComposer,
  useMessageComposerContext,
  useMessageComposerController,
  useStateStore,
  VoiceRecordingPreviewSlot,
  WithDragAndDropUpload,
} from "stream-chat-react";
import "./CustomMessageComposer.css";

const editedMessageSelector = ({ editedMessage }: MessageComposerState) => ({
  editedMessage,
});
const commandSelector = ({ command }: TextComposerState) => ({ command });

// Every action of the default menu except Location.
const attachmentActions = defaultAttachmentSelectorActionSet.filter(
  (action) => action.type !== "addLocation",
);

export const CustomMessageComposer = () => {
  const { recordingController } = useMessageComposerContext();
  const messageComposer = useMessageComposerController();
  const { editedMessage } = useStateStore(
    messageComposer.state,
    editedMessageSelector,
  );
  const { command } = useStateStore(
    messageComposer.textComposer.state,
    commandSelector,
  );

  // The default UI shows the message being edited, with a button to cancel the edit.
  if (editedMessage) return <MessageComposerUI />;

  // While the user records a voice message, the recorder takes the composer's place.
  if (recordingController.recordingState) return <AudioRecorder />;

  return (
    <WithDragAndDropUpload className="custom-composer" component="div">
      {!command && (
        <AttachmentSelector attachmentSelectorActionSet={attachmentActions} />
      )}
      <div className="custom-composer__center">
        <QuotedMessagePreview />
        <VoiceRecordingPreviewSlot />
        <AttachmentPreviewList />
        <LinkPreviewList />
        <div className="custom-composer__input">
          {command && <CommandChip command={command} />}
          <TextareaComposer />
        </div>
      </div>
      <MessageComposerActions />
    </WithDragAndDropUpload>
  );
};
```

```css title="src/CustomMessageComposer.css"
.custom-composer {
  align-items: flex-end;
  display: flex;
  gap: 8px;
  padding: 8px 12px;
}

.custom-composer__center {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.custom-composer__input {
  align-items: center;
  display: flex;
  gap: 8px;
}
```

Here is what each part does:

- `WithDragAndDropUpload` uploads files dropped on the composer, as the default composer does.
- `AttachmentSelector` opens the menu to upload a file, create a poll or pick a command. The menu is an action set, explained in [Actions](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/#attachment-selector-actions).
- The previews render only when the composer has something for them: a quoted message, a voice recording, attachments or link previews.
- `CommandChip` shows the picked command, such as `/giphy`, with a button that clears it.
- `MessageComposerActions` shows one control: the cooldown timer, the record button, the stop button or the send button.

The emoji picker isn't in this layout. To add it, render `AdditionalMessageComposerActions` before `MessageComposerActions`, as the default composer does. It renders the `EmojiPicker` override, explained in [Emoji](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/emoji/).

The parts are imported directly, so overrides registered for them, such as an `AttachmentPreviewList` of your own, don't apply inside this layout. To keep them, read the parts from `useComponentContext()` and fall back to the imports.

## Register it

Register the layout under the `MessageComposerUI` key, around the composers it applies to. Declare the overrides once, so `WithComponents` gets the same object on every render:

```tsx title="src/ChannelWithCustomComposer.tsx"
import {
  Channel,
  ChannelHeader,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";
import { CustomMessageComposer } from "./CustomMessageComposer";

const overrides = { MessageComposerUI: CustomMessageComposer };

export const ChannelWithCustomComposer = () => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);
```

To use the layout in threads too, put `WithComponents` above `ChatView`, or around each `Thread`. [Overriding components](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/overriding-components/) explains how overrides apply.

## Thread replies

In a thread, the default composer shows a checkbox that also posts the reply to the channel. It isn't exported, so a layout of your own adds one. The composer keeps the choice in `showReplyInChannel`, and `threadId` is set only on a thread's composer:

```tsx title="src/AlsoSendInChannel.tsx"
import type { MessageComposerState } from "stream-chat";
import { useMessageComposerController, useStateStore } from "stream-chat-react";

const showReplyInChannelSelector = ({
  showReplyInChannel,
}: MessageComposerState) => ({ showReplyInChannel });

export const AlsoSendInChannel = () => {
  const messageComposer = useMessageComposerController();
  const { showReplyInChannel } = useStateStore(
    messageComposer.state,
    showReplyInChannelSelector,
  );

  if (!messageComposer.threadId) return null;

  return (
    <label className="custom-composer__also-send">
      <input
        checked={showReplyInChannel}
        onChange={() => messageComposer.toggleShowReplyInChannel()}
        type="checkbox"
      />
      Also send in channel
    </label>
  );
};
```

Render `<AlsoSendInChannel />` under the input in `CustomMessageComposer`. It renders nothing in a channel.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/custom-composer/](https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/custom-composer/).