Message Reactions

The Flutter SDK includes a full set of components for adding emoji reactions to messages. This page covers the reaction picker, reaction detail sheet, and paginated reactor list, along with how to customize their appearance and the available set of reactions.

Overview

The reactions system provides a set of components for displaying, picking, and listing reactions. The key components are:

  • StreamMessageReactionPicker — the quick-pick reaction bar that appears on long-press or hover. Shows up to five reactions from defaultReactions plus a "+" button for the full emoji grid.
  • ReactionDetailSheet — a bottom sheet showing total reaction count, per-type filter chips, and a paginated list of who reacted. Opened by tapping the reaction summary bar on a message.
  • StreamReactionListView + StreamReactionListController — paginated reactor list and its data source. Used inside ReactionDetailSheet, and also exported as a public building block for custom reactor UIs.
  • ReactionIconResolver — abstract contract for customizing which reactions appear in the quick-pick bar and the full emoji grid, and how each reaction type is rendered.

StreamMessageReactionPicker

StreamMessageReactionPicker displays the quick-pick reaction bar. The available reactions come from StreamChatConfigurationData.reactionIconResolver.defaultReactions (five reactions by default). A trailing "+" button opens the full emoji grid showing supportedReactions.

Reaction picker bar

StreamMessageReactionPicker(
  message: message,
  onReactionPicked: (reaction) => _addReaction(reaction),
)

Visual customization is done via StreamReactionPickerTheme:

StreamReactionPickerTheme(
  data: StreamReactionPickerThemeData(
    backgroundColor: Colors.white,
    elevation: 4,
    spacing: 2,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(24)),
    ),
  ),
  child: MyApp(),
)

ReactionIconResolver

ReactionIconResolver is an abstract contract for mapping reaction types to emoji or widgets. It controls:

  • defaultReactions — the small set shown in the quick-pick bar
  • supportedReactions — the full set shown in the "+" emoji grid (and the add-emoji chip in ReactionDetailSheet)

Extend DefaultReactionIconResolver and override only what you need:

class MyReactionIconResolver extends DefaultReactionIconResolver {
  const MyReactionIconResolver();

  // Which reactions appear in the quick-pick bar
  @override
  Set<String> get defaultReactions => const {'like', 'love', 'haha', 'wow', 'sad'};

  // Return a Unicode emoji for a reaction type, or null for fallback
  @override
  String? emojiCode(String type) {
    if (type == 'celebrate') return '🎉';
    return super.emojiCode(type); // delegates to streamSupportedEmojis map
  }
}

Register the resolver globally:

StreamChat(
  client: client,
  configData: StreamChatConfigurationData(
    reactionIconResolver: const MyReactionIconResolver(),
  ),
  child: MyHomePage(),
)

Bringing your own emoji set with custom icons

Each reaction is identified by a type string (e.g. 'like', 'grinning'). That key is what gets stored on the message in the backend. The resolver maps each key to its visual representation — a Unicode glyph or a custom image.

Two sets control which reactions are available:

  • defaultReactions — the small set (up to 5) shown in the quick-pick bar.
  • supportedReactions — the full set shown in the "+" emoji grid and in the add-emoji chip inside ReactionDetailSheet. Both the picker grid and the detail sheet chip honor this set.

Here is a complete example that defines a fully custom set with Unicode emoji:

class BrandedReactionResolver extends DefaultReactionIconResolver {
  const BrandedReactionResolver();

  // Map of reaction type key → Unicode emoji.
  // The key is what gets stored on the message; the value is what the user sees.
  static const Map<String, String> _emojiMap = {
    'grinning':  '😀',
    'heart':     '❤️',
    'thumbsup':  '👍',
    'fire':      '🔥',
    'clap':      '👏',
  };

  /// Types shown in the quick-pick bar (max 5).
  @override
  Set<String> get defaultReactions => const {'grinning', 'heart', 'thumbsup'};

  /// Full grid shown when the user taps "+".
  @override
  Set<String> get supportedReactions => _emojiMap.keys.toSet();

  /// Map a type key to its Unicode glyph.
  @override
  String? emojiCode(String type) => _emojiMap[type];
}

Register it once and it applies to both the quick-pick bar and the full grid:

StreamChat(
  client: client,
  configData: StreamChatConfigurationData(
    reactionIconResolver: const BrandedReactionResolver(),
  ),
  child: MyHomePage(),
)

Using custom image icons (e.g. Twemoji or branded PNGs):

Override resolve to return StreamImageEmoji. Every type that returns an image emoji will render that image instead of a Unicode glyph:

class TwemojiReactionResolver extends DefaultReactionIconResolver {
  const TwemojiReactionResolver();

  static const Map<String, String> _emojiMap = {
    'grinning': '😀',
    'heart':    '❤️',
    'thumbsup': '👍',
  };

  @override
  Set<String> get defaultReactions => _emojiMap.keys.toSet();

  @override
  Set<String> get supportedReactions => _emojiMap.keys.toSet();

  @override
  String? emojiCode(String type) => _emojiMap[type];

  @override
  StreamEmojiContent resolve(String type) {
    // Serve a type as an image from your CDN.
    // The URL can encode the type key, a Unicode codepoint, or any slug.
    // Use stillUrl to provide a non-animated fallback when system animations are disabled.
    if(type == 'grinning') {
      return StreamImageEmoji(
        url: Uri.parse('https://my-cdn/emoji/$type.png'),
      );
    }
    // Other types render as Unicode emoji.
    return super.resolve(type);
  }
}

Customizing the full "+" reaction grid

When a user taps the "+" button in the quick-pick bar (or the add-emoji chip in ReactionDetailSheet), the full emoji grid shows all reactions from ReactionIconResolver.supportedReactions. By default this is the entire streamSupportedEmojis catalog.

To show a curated subset, override supportedReactions on your custom resolver:

class MyReactionIconResolver extends DefaultReactionIconResolver {
  const MyReactionIconResolver();

  // Restrict the quick-pick bar to three reactions.
  @override
  Set<String> get defaultReactions => const {'like', 'love', 'haha'};

  // Restrict the full emoji grid ("+") to the same five reactions.
  @override
  Set<String> get supportedReactions => const {'like', 'love', 'haha', 'wow', 'sad'};
}

The same resolver is registered once on StreamChatConfigurationData and applies to both the quick-pick bar and the full grid automatically.

ReactionDetailSheet

ReactionDetailSheet shows a draggable bottom sheet with:

  • Total reaction count
  • Filter chips per reaction type (using the resolver to render each emoji)
  • An add-emoji chip ("+") that opens the same supportedReactions grid
  • Paginated list of users who reacted

Reaction detail sheet

Use the static show method — the constructor is private:

final action = await ReactionDetailSheet.show(
  context: context,
  message: message,
  initialReactionType: 'like', // optional: pre-select a type
);

if (action is SelectReaction) {
  _handleReactionSelected(action.reaction);
}

show returns MessageAction?:

  • SelectReaction — user picked or removed a reaction
  • null — sheet dismissed without selection

StreamReactionListController

StreamReactionListController loads and paginates reactions for a message. It extends PagedValueNotifier<String?, Reaction>.

final controller = StreamReactionListController(
  client: StreamChat.of(context).client,
  messageId: message.id,
  sort: const [SortOption.desc(ReactionSortKey.createdAt)],
  limit: 25,
);

await controller.doInitialLoad();

Filter reactions by type at runtime (e.g. when user taps a filter chip):

controller.filter = Filter.equal('type', 'like');
controller.doInitialLoad();

Reactions display configuration

Two fields on StreamChatConfigurationData control how the reaction summary bar is displayed relative to the message bubble.

reactionType — sets the visual style used to render the reaction summary bar (StreamReactionsType).

reactionPosition — controls where the reaction bar appears relative to the message bubble (StreamReactionsPosition). Position also controls overlap: StreamReactionsPosition.header (default) renders reactions overlapping the top edge of the bubble; StreamReactionsPosition.footer renders them flush below the bubble without overlap.

StreamChat(
  client: client,
  configData: StreamChatConfigurationData(
    reactionType: StreamReactionsType.expanded,
    reactionPosition: StreamReactionsPosition.header,
  ),
  child: MyApp(),
)

StreamReactionListView

StreamReactionListView renders a paginated list of reactions using a StreamReactionListController. The SDK uses it internally inside ReactionDetailSheet to power the scrollable reactor list. It is also exported as a public building block for building custom reactor list UIs outside the detail sheet.

Reaction list view

StreamReactionListView(
  controller: controller,
  itemBuilder: (context, reactions, index) {
    final reaction = reactions[index];
    return ListTile(
      leading: Text(reaction.type),
      title: Text(reaction.user?.name ?? ''),
    );
  },
  emptyBuilder: (_) => const Center(child: Text('No reactions yet')),
)
ParameterRequiredDescription
controlleryesProvides and paginates reaction data
itemBuilderyesBuilds each reaction item
separatorBuildernoBuilds separators between items
emptyBuildernoWidget shown when there are no reactions
loadingBuildernoWidget shown during initial load
errorBuildernoWidget shown on error
loadMoreTriggerIndexnoHow many items from end to trigger next page (default: 3)