# Customizing Widgets

The Flutter SDK provides several patterns for customizing and replacing built-in components. Depending on the scope of your change, you can use builder callbacks on individual widgets, `StreamComponentFactory` for app-wide overrides, or `StreamTheme` / `StreamChatThemeData` for visual styling.

### Props and copyWith

Most SDK widgets accept a `props` argument that is a typed object containing all the data the default implementation uses. You can use this to customize the widget by updating the properties, or creating a fully custom widget based on this data.

- **Per-widget builder callbacks** — when you provide a `messageBuilder` or `itemBuilder`, the callback receives `defaultProps` already populated with all the data for that item. See [Per-widget builder callbacks](#per-widget-builder-callbacks) below for the full list.
- **`StreamComponentFactory` builders** — each slot receives a typed `props` argument with all the data the default implementation uses.

To customize a widget without reimplementing it from scratch, call `copyWith()` to override only the fields you need. Then pass the result to the component's `.fromProps()` factory constructor to rebuild the default widget with your overrides applied:

```dart
StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    // defaultProps is the fully-populated StreamMessageItemProps for this message.
    // copyWith overrides only the fields you care about; everything else stays as-is.
    return StreamMessageItem.fromProps(
      // fromProps rebuilds the default widget using the modified props.
      props: defaultProps.copyWith(
        swipeToReply: true,
        maxWidth: 300,
      ),
    );
  },
)
```

### StreamComponentFactory

`StreamComponentFactory` is the primary extension point for replacing entire component implementations. It uses the `StreamComponentBuilders` API to register custom builders for each slot.

`StreamComponentFactory` is an `InheritedWidget`, so its overrides apply to its subtree only — exactly like Flutter's `Theme`. Place it once at the root of your app to apply custom builders globally, or nest it deeper in the tree to scope overrides to a single screen or section. When multiple `StreamComponentFactory` ancestors are present, the nearest one wins for any given slot.

`StreamComponentBuilders` accepts named slots for both chat-specific components (via `streamChatComponentBuilders`) and core design-system components such as `button`, `avatar`, `textInput`, and `loadingSpinner`:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    // Core component — replace every StreamButton in the subtree.
    button: (context, props) {
      return FilledButton(
        onPressed: props.onPressed,
        style: FilledButton.styleFrom(
          backgroundColor: props.style == StreamButtonStyle.destructive
              ? Colors.red
              : Theme.of(context).colorScheme.primary,
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (props.iconLeft != null) ...[
              Icon(props.iconLeft, size: 18),
              const SizedBox(width: 6),
            ],
            if (props.child != null) props.child!,
          ],
        ),
      );
    },
    // Chat-specific components — passed via the extensions list.
    extensions: streamChatComponentBuilders(
      channelListItem: (context, props) => MyCustomChannelListItem(props: props),
      messageItem: (context, props) => MyCustomMessageWidget(props: props),
    ),
  ),
  child: MyApp(),
)
```

Each builder receives a typed props object containing all the data the default implementation uses. `StreamButtonProps`, for example, exposes `child`, `onPressed`, `style`, `type`, `size`, `iconLeft`, `iconRight`, `isSelected`, `isFloating`, `themeStyle`.

Spread multiple extension sets together when composing overrides from different sources:

```dart
extensions: [
  ...streamChatComponentBuilders(
    channelListItem: (context, props) => MyChannelListItem(props: props),
  ),
  ...myOtherBuilders(),
]
```

The nearest `StreamComponentFactory` ancestor wins, so you can scope an override to a single screen without affecting the rest of the app.

#### What is a builder key?

A **builder key** is the slot name you pass to `StreamComponentBuilders` or `streamChatComponentBuilders(...)`. Each key maps to a specific component type. Your builder receives a typed `Props` object containing the data the default implementation uses. You return the widget you want rendered in its place.

**Full signature pattern:**

```dart
// Core component (direct named parameter on StreamComponentBuilders)
StreamComponentBuilders(
  button: (BuildContext context, StreamButtonProps props) {
    return FilledButton(onPressed: props.onPressed, child: props.child);
  },
)

// Chat-specific component (via the extensions list)
StreamComponentBuilders(
  extensions: streamChatComponentBuilders(
    messageItem: (BuildContext context, StreamMessageItemProps props) {
      // Use props.copyWith() to adjust defaults, or return a completely custom widget.
      return DefaultStreamMessageItem(
        props: props.copyWith(
          actionsBuilder: (context, defaultActions) => [
            ...defaultActions,
            StreamContextMenuAction(
              leading: const Icon(Icons.star),
              label: const Text('Favourite'),
              onTap: () => _favourite(props.message),
            ),
          ],
        ),
      );
    },
  ),
)
```

See the [Props and copyWith](#props-and-copywith) section above for details on working with props classes.

#### Core components (stream_core_flutter)

These are design-system primitives shared across Stream SDKs. Pass them directly as named parameters on `StreamComponentBuilders`.

| Builder key                            | Props class                                       | Description                                        |
| -------------------------------------- | ------------------------------------------------- | -------------------------------------------------- |
| `appBar`                               | `StreamAppBarProps`                               | App bar                                            |
| `avatar`                               | `StreamAvatarProps`                               | Single avatar image                                |
| `avatarGroup`                          | `StreamAvatarGroupProps`                          | Overlapping group of avatars                       |
| `avatarStack`                          | `StreamAvatarStackProps`                          | Stacked avatar layout                              |
| `badgeCount`                           | `StreamBadgeCountProps`                           | Numeric unread/count badge                         |
| `badgeNotification`                    | `StreamBadgeNotificationProps`                    | Notification dot badge                             |
| `bottomAppBar`                         | `StreamBottomAppBarProps`                         | Bottom app bar                                     |
| `button`                               | `StreamButtonProps`                               | Button                                             |
| `checkbox`                             | `StreamCheckboxProps`                             | Checkbox                                           |
| `commandChip`                          | `StreamCommandChipProps`                          | Slash-command suggestion chip                      |
| `contextMenuAction`                    | `StreamContextMenuActionProps`                    | Item inside a context menu                         |
| `emoji`                                | `StreamEmojiProps`                                | Emoji renderer                                     |
| `emojiButton`                          | `StreamEmojiButtonProps`                          | Button that opens the emoji picker                 |
| `emojiChip`                            | `StreamEmojiChipProps`                            | Single emoji chip (reaction)                       |
| `emojiChipBar`                         | `StreamEmojiChipBarProps`                         | Row of emoji chips                                 |
| `errorBadge`                           | `StreamErrorBadgeProps`                           | Error badge overlay                                |
| `fileTypeIcon`                         | `StreamFileTypeIconProps`                         | Icon representing a file type                      |
| `imageSourceBadge`                     | `StreamImageSourceBadgeProps`                     | Badge showing the image source                     |
| `jumpToUnreadButton`                   | `StreamJumpToUnreadButtonProps`                   | Floating button to jump to first unread            |
| `listTile`                             | `StreamListTileProps`                             | Generic list tile                                  |
| `loadingSpinner`                       | `StreamLoadingSpinnerProps`                       | Loading spinner                                    |
| `mediaViewer`                          | `StreamMediaViewerProps`                          | Full-screen media viewer                           |
| `messageAnnotation`                    | `StreamMessageAnnotationProps`                    | System/annotation message row                      |
| `messageBubble`                        | `StreamMessageBubbleProps`                        | Message bubble container                           |
| `messageComposerAttachment`            | `StreamMessageComposerAttachmentProps`            | Generic attachment chip in the composer            |
| `messageComposerEditMessageAttachment` | `StreamMessageComposerEditMessageAttachmentProps` | Edit-message preview in the composer               |
| `messageComposerFileAttachment`        | `StreamMessageComposerFileAttachmentProps`        | File attachment preview in the composer            |
| `messageComposerLinkPreviewAttachment` | `StreamMessageComposerLinkPreviewAttachmentProps` | Link preview in the composer                       |
| `messageComposerMediaAttachment`       | `StreamMessageComposerMediaAttachmentProps`       | Image/video preview in the composer                |
| `messageComposerReplyAttachment`       | `StreamMessageComposerReplyAttachmentProps`       | Reply preview in the composer                      |
| `messageComposerUnsupportedAttachment` | `StreamMessageComposerUnsupportedAttachmentProps` | Unsupported attachment placeholder in the composer |
| `messageContent`                       | `StreamMessageContentProps`                       | Message content layout (bubble + metadata)         |
| `messageMetadata`                      | `StreamMessageMetadataProps`                      | Timestamp and delivery status row                  |
| `messageReplies`                       | `StreamMessageRepliesProps`                       | Thread reply count link                            |
| `messageText`                          | `StreamMessageTextProps`                          | Message body (markdown renderer)                   |
| `networkImage`                         | `StreamNetworkImageProps`                         | Network image with loading/error states            |
| `onlineIndicator`                      | `StreamOnlineIndicatorProps`                      | Online presence dot                                |
| `playbackSpeedToggle`                  | `StreamPlaybackSpeedToggleProps`                  | Audio playback speed button                        |
| `progressBar`                          | `StreamProgressBarProps`                          | Audio/upload progress bar                          |
| `reactionPicker`                       | `StreamReactionPickerProps`                       | Reaction picker overlay                            |
| `reactions`                            | `StreamReactionsProps`                            | Reaction row beneath a message                     |
| `retryBadge`                           | `StreamRetryBadgeProps`                           | Failed-send retry badge                            |
| `sheetHeader`                          | `StreamSheetHeaderProps`                          | Bottom sheet header/handle                         |
| `skeletonLoading`                      | `StreamSkeletonLoadingProps`                      | Shimmer skeleton loading placeholder               |
| `stepper`                              | `StreamStepperProps`                              | Numeric increment/decrement stepper                |
| `textInput`                            | `StreamTextInputProps`                            | Text input field                                   |
| `toggleSwitch`                         | `StreamSwitchProps`                               | Toggle switch                                      |

#### Chat-specific components (stream_chat_flutter)

These are chat domain components registered via `streamChatComponentBuilders(...)` passed to the `extensions` list.

**Channel and thread list**

| Builder key       | Props class                  | Description             |
| ----------------- | ---------------------------- | ----------------------- |
| `channelListItem` | `StreamChannelListItemProps` | Row in the channel list |
| `threadListItem`  | `StreamThreadListTileProps`  | Row in the thread list  |

Example — custom channel list item:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      channelListItem: (context, props) {
        return StreamChannelListTile(
          avatar: StreamChannelAvatar(channel: props.channel),
          title: Text(props.channel.name ?? ''),
          subtitle: Text(props.channel.lastMessageAt?.toString() ?? ''),
          onTap: props.onTap,
          onLongPress: props.onLongPress,
          selected: props.selected,
        );
      },
    ),
  ),
  child: MyApp(),
)
```

**Message list**

| Builder key      | Props class                 | Description                                                               |
| ---------------- | --------------------------- | ------------------------------------------------------------------------- |
| `messageItem`    | `StreamMessageItemProps`    | Individual message item                                                   |
| `messageLeading` | `StreamMessageLeadingProps` | Leading slot of the message row (avatar area for other users' messages)   |
| `messageHeader`  | `StreamMessageHeaderProps`  | Header row above the bubble (sender name, thread-parent label, etc.)      |
| `messageFooter`  | `StreamMessageFooterProps`  | Footer row below the bubble (timestamp, delivery state, edited indicator) |

Example — add a custom context-menu action to every message:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageItem: (context, props) {
        return DefaultStreamMessageItem(
          props: props.copyWith(
            actionsBuilder: (context, defaultActions) => [
              ...defaultActions,
              StreamContextMenuAction(
                leading: const Icon(Icons.star),
                label: const Text('Favourite'),
                onTap: () => _favourite(props.message),
              ),
            ],
          ),
        );
      },
    ),
  ),
  child: MyApp(),
)
```

**Message composer**

| Builder key                     | Props class                                | Description                             |
| ------------------------------- | ------------------------------------------ | --------------------------------------- |
| `messageComposer`               | `MessageComposerProps`                     | Full composer widget                    |
| `messageComposerLeading`        | `MessageComposerLeadingProps`              | Left side of the composer bar           |
| `messageComposerTrailing`       | `MessageComposerTrailingProps`             | Right side of the composer bar          |
| `messageComposerInput`          | `MessageComposerInputProps`                | Input field area                        |
| `messageComposerInputCenter`    | `MessageComposerInputCenterProps`          | Center section of the input field       |
| `messageComposerInputLeading`   | `MessageComposerInputLeadingProps`         | Leading icon(s) inside the input field  |
| `messageComposerInputHeader`    | `MessageComposerInputHeaderProps`          | Header row above the input field        |
| `messageComposerInputTrailing`  | `MessageComposerInputTrailingProps`        | Trailing icon(s) inside the input field |
| `messageComposerAttachmentList` | `StreamMessageComposerAttachmentListProps` | Horizontal list of attachment previews  |
| `messageComposerAttachment`     | `StreamMessageComposerAttachmentProps`     | Individual attachment preview chip      |

**Message composer**

Example — move the send button outside the text input and add an emoji picker inside it:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      // Remove send button from inside the input field
      messageComposerInputTrailing: (context, props) => const SizedBox.shrink(),
      // Render send button outside (to the right of the whole composer bar)
      messageComposerTrailing: (context, props) =>
          DefaultStreamMessageComposerInputTrailing(props: props),
      // Add an emoji button inside the input on the left
      messageComposerInputLeading: (context, props) => StreamButton.icon(
        icon: Icon(context.streamIcons.emoji),
        type: StreamButtonType.ghost,
        style: StreamButtonStyle.secondary,
        size: StreamButtonSize.small,
        onPressed: () { /* open emoji picker */ },
      ),
    ),
  ),
  child: StreamMessageComposer(),
)
```

**Attachments**

| Builder key                | Props class                           | Description                                |
| -------------------------- | ------------------------------------- | ------------------------------------------ |
| `imageAttachment`          | `StreamImageAttachmentProps`          | Single image attachment                    |
| `videoAttachment`          | `StreamVideoAttachmentProps`          | Video attachment with playback             |
| `giphyAttachment`          | `StreamGiphyAttachmentProps`          | Giphy/GIF attachment                       |
| `galleryAttachment`        | `StreamGalleryAttachmentProps`        | Multi-image gallery attachment             |
| `fileAttachment`           | `StreamFileAttachmentProps`           | Generic file attachment                    |
| `linkPreviewAttachment`    | `StreamLinkPreviewAttachmentProps`    | Link preview card                          |
| `voiceRecordingAttachment` | `StreamVoiceRecordingAttachmentProps` | Voice recording playback attachment        |
| `pollAttachment`           | `StreamPollAttachmentProps`           | Poll attachment                            |
| `quotedMessage`            | `StreamQuotedMessageProps`            | Quoted/replied message preview             |
| `unsupportedAttachment`    | `StreamUnsupportedAttachmentProps`    | Fallback for unrecognised attachment types |
| `mediaGallery`             | `StreamMediaGalleryProps`             | Full-screen media gallery                  |
| `mediaGalleryPreview`      | `StreamMediaGalleryPreviewProps`      | Media gallery preview thumbnail strip      |

Note: The factory slots above replace the pre-built attachment _widget_ for that type everywhere in the app. If you need to add a _new_ custom attachment type or replace how built-in types render per-message, use `StreamAttachmentWidgetBuilder` instead — see [Custom Attachment Builders](/chat/docs/sdk/flutter/stream-chat-flutter/message-list/stream-message-item/#custom-attachment-builders).

### Per-widget builder callbacks

Many high-level widgets expose `itemBuilder` or `messageBuilder` callbacks for customizing individual items without replacing the whole component:

```dart
StreamChannelListView(
  controller: _controller,
  itemBuilder: (context, channels, index, defaultTile) {
    final channel = channels[index];
    return ListTile(
      title: Text(channel.name ?? ''),
      onTap: () => openChannel(channel),
    );
  },
)
```

```dart
StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    return StreamMessageItem.fromProps(
      props: defaultProps.copyWith(
        actionsBuilder: (context, defaultActions) => [
          ...defaultActions,
          StreamContextMenuAction(
            leading: const Icon(Icons.star),
            label: const Text('Favourite'),
            onTap: () => _favourite(message),
          ),
        ],
      ),
    );
  },
)
```

### Theming vs. component replacement

Use theming for visual changes (colors, typography, spacing) and component replacement for structural or behavioral changes.

| Goal                                 | Approach                                  |
| ------------------------------------ | ----------------------------------------- |
| Change colors, fonts, sizing         | `StreamTheme` / `StreamChatThemeData`     |
| Hide or reorder parts of a component | `copyWith()` on the props class           |
| Replace a component entirely         | `StreamComponentFactory` builder          |
| Customize a single list item         | `itemBuilder` / `messageBuilder` callback |

See the [Theming](/chat/docs/sdk/flutter/stream-chat-flutter/stream-chat-and-theming/) page for details on visual customization.


---

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

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