# StreamMessageComposer

`StreamMessageComposer` is the full-featured message composer widget, handling text input, media attachments, mentions, commands, quoted replies, and more out of the box. This page explains how to add it to a channel screen, configure its behavior, and customize its appearance. See the [pub.dev documentation](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamMessageComposer-class.html) for the full API reference.

![](@chat-sdk/flutter/v10-latest/_assets/message_composer.png)

### Background

In Stream Chat, we can send messages in a channel. However, sending a message isn't as simple as adding
a `TextField` and logic for sending a message. It involves additional processes like addition of media,
quoting a message, adding a custom command like a GIF board, and much more. Moreover, most apps also
need to customize the input to match their theme, overall color and structure pattern, etc.

To do this, we created a `StreamMessageComposer` widget which abstracts all expected functionality a
modern composer needs and allows you to use it out of the box. The widget is responsible for the full
composition flow: typing, sending messages, attaching files, recording voice messages, mentioning
users, running slash commands, and more.

### Basic Example

A `StreamChannel` is required above the widget tree in which the `StreamMessageComposer` is rendered since the channel is
where the messages sent actually go. Let's look at a common example of how we could use the `StreamMessageComposer`:

```dart
class ChannelPage extends StatelessWidget {
  const ChannelPage({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const StreamChannelHeader(),
      body: Column(
        children: <Widget>[
          Expanded(
            child: StreamMessageListView(
              threadBuilder: (_, parentMessage) {
                return ThreadPage(
                  parent: parentMessage,
                );
              },
            ),
          ),
          StreamMessageComposer(),
        ],
      ),
    );
  }
}
```

It is common to put this widget in the same page of a `StreamMessageListView` as the bottom widget.

<admonition type="note">

Make sure to check the [StreamMessageComposerController](/chat/docs/sdk/flutter/stream-chat-flutter-core/stream-message-composer-controller/) documentation for more information on how to use the controller to manipulate the `StreamMessageComposer`.

</admonition>

### Adding Custom Actions

Custom actions can be added to the composer using the component builder system via `StreamComponentFactory`. The `messageComposerInputLeading` slot is empty by default and is designed for adding custom action buttons inside the text input field — to the left of the text area.

Register a custom builder using `streamChatComponentBuilders` and pass it to `StreamChat` (or `StreamComponentFactory` if scoped to a single screen):

```dart
StreamChat(
  client: client,
  componentBuilders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageComposerInputLeading: (context, props) {
        return IconButton(
          icon: Icon(
            Icons.location_on,
            color: StreamTheme.of(context).colorScheme.textSecondary,
          ),
          onPressed: () {
            // Do something here
          },
        );
      },
    ),
  ),
  child: MyApp(),
)
```

If you only want the custom action on a specific screen, wrap that screen with `StreamComponentFactory` instead:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageComposerInputLeading: (context, props) {
        return IconButton(
          icon: Icon(
            Icons.location_on,
            color: StreamTheme.of(context).colorScheme.textSecondary,
          ),
          onPressed: () {
            // Do something here
          },
        );
      },
    ),
  ),
  child: StreamMessageComposer(),
)
```

The `props` parameter (of type `MessageComposerInputLeadingProps`) exposes the full composer state, including the `controller`, `onAttachmentButtonPressed`, `focusNode`, and other callbacks you may need in your custom action.

### Disable Attachments

To disable attachments being added to the message, set the `disableAttachments` parameter to true.

```dart
StreamMessageComposer(
    disableAttachments: true,
),
```

### Changing Position of Message Composer Components

You can change the position of the send button and actions by using `componentBuilders` via `StreamComponentBuilders`.

For example, to move the send button outside the TextField (to the trailing position), use `messageComposerInputTrailing` to remove it from inside the input, and `messageComposerTrailing` to render it outside:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageComposerInputTrailing: (context, props) =>
          const SizedBox.shrink(),
      messageComposerTrailing: (context, props) =>
          DefaultStreamMessageComposerInputTrailing(props: props),
    ),
  ),
  child: StreamMessageComposer(),
)
```

![](@chat-sdk/flutter/v10-latest/_assets/message_composer_change_position.png)

### Custom composer layout

You can combine multiple slots to create a fully custom layout: an emoji icon inside the input on the left, an attachment button inside the input on the right, and a mic/send button that floats outside the input field and switches based on whether there is text.

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageComposerLeading: (context, props) => const SizedBox.shrink(),
      messageComposerInputLeading: (context, props) => StreamButton.icon(
        icon: Icon(context.streamIcons.emoji),
        type: StreamButtonType.ghost,
        style: StreamButtonStyle.secondary,
        size: StreamButtonSize.small,
        onPressed: () {
          // Open emoji picker
        },
      ),
      messageComposerInputTrailing: (context, props) => StreamButton.icon(
        icon: Icon(context.streamIcons.attachment),
        type: StreamButtonType.ghost,
        style: StreamButtonStyle.secondary,
        size: StreamButtonSize.small,
        onPressed: props.onAttachmentButtonPressed,
      ),
      messageComposerTrailing: (context, props) =>
          CustomComposerTrailingButton(props: props),
    ),
  ),
  child: StreamMessageComposer(
    placeholderBuilder: (context, placeholder) => switch (placeholder) {
      WriteMessagePlaceholder() => 'Type a message...',
      _ => null, // fall back to default for other placeholder types
    },
  ),
)
```

`CustomComposerTrailingButton` listens to the controller and shows a mic button when the input is empty, switching to a send button as soon as the user starts typing:

```dart
class CustomComposerTrailingButton extends StatefulWidget {
  const CustomComposerTrailingButton({super.key, required this.props});

  final MessageComposerComponentProps props;

  @override
  State<CustomComposerTrailingButton> createState() =>
      _CustomComposerTrailingButtonState();
}

class _CustomComposerTrailingButtonState
    extends State<CustomComposerTrailingButton> {
  var _isEmptyText = true;

  @override
  void initState() {
    super.initState();
    widget.props.controller.addListener(_updateIsEmptyText);
  }

  void _updateIsEmptyText() {
    final isEmptyText = widget.props.controller.text.trim().isEmpty;
    if (_isEmptyText != isEmptyText) {
      setState(() => _isEmptyText = isEmptyText);
    }
  }

  @override
  void dispose() {
    widget.props.controller.removeListener(_updateIsEmptyText);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _isEmptyText
        ? StreamButton.icon(
            icon: Icon(context.streamIcons.voice),
            type: StreamButtonType.solid,
            style: StreamButtonStyle.primary,
            size: StreamButtonSize.medium,
            onPressed: () {
              // Start voice recording
            },
          )
        : StreamButton.icon(
            icon: Icon(context.streamIcons.send),
            type: StreamButtonType.solid,
            style: StreamButtonStyle.primary,
            size: StreamButtonSize.medium,
            onPressed: widget.props.onSendPressed,
          );
  }
}
```

![](@chat-sdk/flutter/v10-latest/_assets/message_composer_custom_buttons.png)

### Adding content above the input field

Use the `messageComposerInputHeader` slot to render content above the text entry row, inside the bordered composer surface. The default implementation renders the quoted-message preview, the attachment preview strip, and the OG link preview. Override it to add a custom banner, a status bar, or additional controls.

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageComposerInputHeader: (context, props) {
        // Replace the default header with a custom banner.
        // The default header renders quoted-message previews, attachment
        // previews, and OG link previews. Override it here and handle those
        // states yourself if needed (check props.controller.quotedMessage,
        // props.controller.ogAttachment, etc.) or omit them if you don't
        // need that behaviour.
        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          color: Colors.amber.shade50,
          child: const Row(
            children: [
              Icon(Icons.warning_amber_rounded, size: 16),
              SizedBox(width: 8),
              Text('This channel is moderated', style: TextStyle(fontSize: 12)),
            ],
          ),
        );
      },
    ),
  ),
  child: StreamMessageComposer(),
)
```

`MessageComposerInputHeaderProps` exposes the full composer state via `props.controller`, so you can conditionally show or hide header content based on whether there is a quoted message, ongoing attachments, or a link preview.

![](@chat-sdk/flutter/v10-latest/_assets/message_composer_input_header.png)

### Thread: "Also Send to Channel" Checkbox

Use `canAlsoSendToChannelFromThread` to control whether the "also send to channel" checkbox appears in threads. It defaults to `true`.

```dart
// Hide the "also send to channel" checkbox
StreamMessageComposer(
  canAlsoSendToChannelFromThread: false,
)
```

### Slow Mode

When slow mode is enabled on a channel, users must wait a cooldown period between messages. The composer automatically reflects this state: the send button is disabled and the input placeholder shows a live countdown until the user can send again.

![](@chat-sdk/flutter/v10-latest/_assets/message_composer_slow_mode.png)

No additional configuration is needed — the composer reads the remaining cooldown from the channel and updates the UI automatically.


---

This page was last updated at 2026-06-09T15:44:07.728Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter/message-composer/stream-message-composer/](https://getstream.io/chat/docs/sdk/flutter/stream-chat-flutter/message-composer/stream-message-composer/).