flutter_chat_reactions 1.0.0
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 #
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 |
|---|---|---|
![]() |
![]() |
![]() |
| Custom (headless) | Theming | Dark mode |
![]() |
![]() |
![]() |
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
ReactionsControllerwith 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_flutteradapter 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 gallery #
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 ❤️
License #
See LICENSE.







