flutter_chat_reactions 1.0.0 copy "flutter_chat_reactions: ^1.0.0" to clipboard
flutter_chat_reactions: ^1.0.0 copied to clipboard

Add reactions and a context menu to chat messages in Flutter, with adaptive iOS and Material styling.

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 ❤️

Buy Me A Coffee

License #

See LICENSE.

52
likes
160
points
1.03k
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Add reactions and a context menu to chat messages in Flutter, with adaptive iOS and Material styling.

Repository (GitHub)
View/report issues
Contributing

Topics

#chat #reactions #emoji #context-menu #messaging

License

GPL-3.0 (license)

Dependencies

flutter

More

Packages that depend on flutter_chat_reactions