flutter_chat_reactions

pub package CI License

Reactions and context menus for chat messages — iMessage, WhatsApp, Slack, Telegram or fully custom — for any chat app and any backend. Zero dependencies.

Messenger Team channel Telegram-like
Messenger Team channel Telegram-like
Custom (headless) Theming Dark mode
Custom Theming Dark

Features

  • Works with any chat app. Your app owns the data: pass each message's reactions in, get taps out. Firebase, Supabase, Stream, your own server, Riverpod, Bloc or setState — all fine.
  • Four presentations, all swappable: focused overlay (default), compact bar, bottom sheet, or your own UI via CustomPresenter.
  • Adaptive look: Cupertino on iOS/macOS, Material 3 elsewhere — or force either. Themed with a standard ThemeExtension, light and dark.
  • Every input: long-press, double-tap, right-click, hover, keyboard, and screen-reader actions.
  • Accessible: semantics labels, keyboard navigation, right-to-left layouts, reduced motion.
  • Optional ReactionsController with one-per-user or many-per-user policies and optimistic updates that roll back if your backend call fails.
  • Zero dependencies. Bring any emoji picker (an emoji_picker_flutter adapter is in the example).

Install

dependencies:
  flutter_chat_reactions: ^1.0.0 # x-release-please-version

Requires Flutter 3.32 or newer.

Quick start

import 'package:flutter/material.dart';
import 'package:flutter_chat_reactions/flutter_chat_reactions.dart';

void main() => runApp(const QuickStartApp());

class QuickStartApp extends StatefulWidget {
  const QuickStartApp({super.key});

  @override
  State<QuickStartApp> createState() => _QuickStartAppState();
}

class _QuickStartAppState extends State<QuickStartApp> {
  final _controller = ReactionsController(
    currentUserId: 'me',
    currentUserName: 'You',
  );
  final _messageIds = ['m1', 'm2'];

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Quick start')),
        body: ListenableBuilder(
          listenable: _controller,
          builder: (context, _) => ListView(
            padding: const EdgeInsets.all(16),
            children: [
              for (final id in _messageIds)
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    ReactableMessage(
                      reactions: _controller.summariesFor(id),
                      onReactionSelected:
                          _controller.bind(id).onReactionSelected,
                      child: Text('Message $id'),
                    ),
                    ReactionsSummaryView(
                      reactions: _controller.summariesFor(id),
                      onReactionTap: _controller.bind(id).onReactionSelected,
                    ),
                  ],
                ),
            ],
          ),
        ),
      ),
    );
  }
}

Long-press a message (right-click on desktop) to react. The runnable version is example/lib/demos/quick_start_demo.dart.

Your data, your backend

The widgets only display what you pass and report what the user tapped:

ReactableMessage(
  reactions: [
    for (final r in message.reactions) // aggregated by your server
      ReactionSummary(emoji: r.emoji, count: r.count, reactedByMe: r.mine),
  ],
  onReactionSelected: (emoji) => api.toggleReaction(message.id, emoji),
  child: MessageBubble(message),
)

Storing individual reactions instead? Aggregate them with reactions.summarize(currentUserId: me.id).

Want the state handled for you? ReactionsController keeps reactions in memory, applies changes immediately, and rolls back if your onChange throws:

final controller = ReactionsController(
  currentUserId: me.id,
  policy: const ReactionPolicy.multiple(max: 3), // or ReactionPolicy.single()
  onChange: (change) => switch (change) {
    ReactionAdded(:final messageId, :final emoji) => api.add(messageId, emoji),
    ReactionRemoved(:final messageId, :final emoji) => api.remove(messageId, emoji),
    ReactionReplaced(:final messageId, :final emoji, :final previousEmoji) =>
      api.replace(messageId, previousEmoji, emoji),
  },
);

final binding = controller.bind(message.id);
ReactableMessage(
  reactions: binding.reactions,
  onReactionSelected: binding.onReactionSelected,
  child: MessageBubble(message),
);

Presenters

Presenter Looks like Notes
FocusedOverlayPresenter (default) iMessage / WhatsApp Blurred backdrop, message lifts, bar above, actions below. Falls back to the bottom sheet at large text sizes.
CompactBarPresenter Slack / Discord Small floating bar; opens on hover on desktop; "⋯" reveals actions.
BottomSheetPresenter Telegram / Material Sheet with the reaction row, optional who-reacted list, full-width actions; Cupertino action sheet on iOS.
CustomPresenter Anything You draw it; the package handles the route, barrier, Escape/back and focus.

Set one per message (ReactableMessage.presenter) or for a whole chat:

ChatReactionsScope(
  presenter: const CompactBarPresenter(),
  quickReactions: const ['👍', '🎉', '👀', '✅'],
  actionsBuilder: (context) => const [
    ReactionAction<void>(id: 'reply', label: 'Reply', icon: Icons.reply),
    ReactionAction<void>(id: 'delete', label: 'Delete', icon: Icons.delete, isDestructive: true),
  ],
  child: ChatList(),
)

Headless example — a bar anchored to the message, built from the public building blocks:

CustomPresenter(
  barrierColor: Colors.black54,
  builder: (context, menu, animation) => AnchoredLayout(
    anchorRect: menu.anchorRect,
    header: FadeTransition(
      opacity: animation,
      child: ReactionBar(
        reactions: menu.quickReactions,
        selected: menu.selectedReactions,
        onSelected: menu.selectReaction,
      ),
    ),
  ),
)

See the radial picker in example/lib/widgets/radial_reaction_menu.dart for a complete custom UI.

Showing reactions

ReactionsSummaryView(
  reactions: reactions,
  layout: ReactionSummaryLayout.chips, // .stacked (WhatsApp) or .compact
  maxVisible: 5,
  onReactionTap: (emoji) => toggle(emoji),
  onTap: () => showReactionDetails(context, reactions),
)

// Overlapping the bubble's bottom edge, WhatsApp style:
ReactionsSummaryView.overlay(reactions: reactions, child: MessageBubble(message))

Triggers

Platform Default triggers
iOS, Android long-press, keyboard
macOS, Windows, Linux, web on desktop right-click, hover (compact bar only), keyboard

Override per message or scope: triggers: {ReactionTrigger.doubleTap, ReactionTrigger.longPress}. Every message also exposes an "Open reactions menu" accessibility action.

Theming

MaterialApp(
  theme: ThemeData(
    colorSchemeSeed: Colors.indigo,
    extensions: const [
      ChatReactionsTheme(
        style: ReactionsVisualStyle.cupertino, // .material, or .adaptive (default)
        barStyle: ReactionBarStyle(emojiSize: 32),
        chipStyle: ReactionChipStyle(borderRadius: BorderRadius.all(Radius.circular(8))),
        haptics: ReactionHaptics.none,
      ),
    ],
  ),
)

Unset values come from your ColorScheme and TextTheme, so light and dark mode work out of the box. A Theme placed around part of your UI also applies to the menus opened from there.

Custom emoji and emoji pickers

Emoji are plain strings, so server-side emoji like :party: work — draw them with emojiBuilder (on ReactableMessage, ChatReactionsScope and ReactionsSummaryView). The package ships no emoji picker: show any picker from onMoreTap and pass the result to your selection handler. The example's emoji_picker_sheet.dart wires up emoji_picker_flutter in about 20 lines.

Localization

English strings are built in. Translate by overriding what you need:

class GermanReactions extends DefaultChatReactionsLocalizations {
  const GermanReactions();
  @override
  String get moreReactions => 'Weitere Reaktionen';
  @override
  String emojiLabel(String emoji) =>
      emoji == ':party:' ? 'Party' : super.emojiLabel(emoji);
}

ChatReactionsScope(localizations: const GermanReactions(), child: ChatList())

emojiLabel is also how custom :shortcode: emoji get readable screen-reader names.

example/ contains six demos — Quick start, Messenger, Team channel, Telegram-like, Custom and Theming playground:

cd example
flutter run --dart-define=DEMO=team --dart-define=THEME=dark

Upgrading from 0.2.x

1.0 is a new API. See MIGRATION.md for a mapping of every 0.2.x class and option.

Contributing

Contributions are welcome. CI runs formatting, analysis, tests (≥ 90% coverage), the pub.dev score, golden tests and the example on Flutter 3.32 and stable. See RELEASING.md for how releases, golden images and the demo GIFs are produced.

Support

Liked some of my work? Buy me a coffee. Thanks for your support :heart:

Buy Me A Coffee

License

See LICENSE.

Libraries

flutter_chat_reactions
Reactions and context menus for chat messages.