Skip to content

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 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:

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:

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:

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:

// 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 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:

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:

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:

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
videoPlayer StreamVideoPlayerProps Video playback inside the media gallery

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.

Per-widget builder callbacks

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

StreamChannelListView(
  controller: _controller,
  itemBuilder: (context, channels, index, defaultTile) {
    final channel = channels[index];
    return ListTile(
      title: Text(channel.name ?? ''),
      onTap: () => openChannel(channel),
    );
  },
)
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 page for details on visual customization.