flutter_preview_context_menu
A flexible preview context menu for Flutter inspired by modern messaging apps. It is designed for chat bubbles, media previews, list rows, files, cards, or any widget that needs long-press interaction on touch and secondary-click interaction on mouse or trackpad devices.
Features
- Long-press and secondary-click context menus for any widget.
- Optional custom preview, separate from the pressed child.
- Optional top content for reactions, chips, shortcuts, or any custom widget.
- Configurable action rows with icons, descriptions, trailing widgets, disabled states, separators, and destructive styling.
- Natural-height action panels that never clip or create an internal scroll area.
- Pinned action panels with independently scrollable top content and previews when vertical space is limited.
- Viewport-aware positioning so the preview and actions stay on screen when possible.
- Automatic horizontal alignment by default: menus stay centered on the pressed widget when they fit, then align to the closest source edge near screen boundaries.
- Configurable panel shape, background color, foreground color, elevation, barrier color, and blur.
- Keyboard-aware focus handling: focused inputs are unfocused while the menu is open and restored when it closes.
- One coherent directional entrance animation for the preview, optional top content, and action panel.
- Widget-based custom actions through
actionChildren, useful when actions need keys or fully custom layouts. - No app-specific dependencies.
Installation
dependencies:
flutter_preview_context_menu: ^0.4.0
Then import the package:
import 'package:flutter_preview_context_menu/flutter_preview_context_menu.dart';
Basic usage
ContextMenuRegion(
actions: [
ContextMenuActionData(
label: 'Reply',
icon: const Icon(Icons.reply),
onPressed: () {
// Run your own logic here.
},
),
ContextMenuActionData(
label: 'Delete',
icon: const Icon(Icons.delete_outline),
destructive: true,
separatedBefore: true,
onPressed: () {
// Confirm or delete from the caller.
},
),
],
child: const Text('Open context menu'),
)
The same region opens with a long press on touch devices and a secondary click on desktop or web. No platform branching is required.
alignment is optional. When omitted, the menu chooses a dynamic alignment from
the pressed widget position. Pass AlignmentDirectional.centerStart,
AlignmentDirectional.center, or AlignmentDirectional.centerEnd only when the
caller needs a fixed layout.
Touch and pointer activation
Secondary-click support is enabled by default anywhere Flutter reports a secondary pointer button. It can be disabled independently while preserving long press:
ContextMenuRegion(
openOnSecondaryTap: false,
onLongPressStart: (details) {
// Observe touch or primary-button long press.
},
actions: actions,
child: child,
)
Use onSecondaryTapDown when the application needs pointer details for
analytics or coordination with another interaction:
ContextMenuRegion(
onSecondaryTapDown: (details) {
debugPrint('Opened at ${details.globalPosition}');
},
actions: actions,
child: child,
)
Appearance
The action panel uses ColorScheme.surfaceContainer by default and keeps the
same rounded shape used by modern messaging menus. Separators follow the active
theme divider color, and elevation defaults to 0 for a flat iOS-like feel.
Override only what your app needs:
ContextMenuRegion(
panelShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
panelBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
panelForegroundColor: Theme.of(context).colorScheme.onSurface,
panelElevation: 8,
barrierColor: Colors.black.withValues(alpha: 0.20),
blur: 14,
actions: actions,
child: child,
)
Custom preview
The preview defaults to child. Provide preview when the menu should show a different or more compact widget.
ContextMenuRegion(
child: messageBubble,
preview: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 280),
child: messageBubblePreview,
),
actions: actions,
)
Widget-based actions
Use actionChildren when an action needs a Key, a custom layout, or behavior that does not fit ContextMenuActionData.
ContextMenuRegion(
actionChildren: [
ContextMenuAction(
key: const ValueKey('reply-action'),
label: 'Reply',
icon: const Icon(Icons.reply),
onPressed: () {
// The menu closes before this callback runs.
},
),
],
child: messageBubble,
)
Reactions or top content
topContent accepts any widget. This keeps reactions and shortcuts outside of the package’s business logic.
ContextMenuRegion(
topContent: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
),
child: const Text('👍 ❤️ 😂 😮 😢 🙏'),
),
actions: actions,
child: messageBubble,
)
Overflow behavior
Action panels always use their complete natural height. They are never clipped, height-limited, or made internally scrollable.
The default pinActions strategy adapts to the available viewport:
- When the complete menu fits, the panel remains below the preview.
- When the complete menu does not fit but the panel does, the panel stays fixed
at the bottom of the usable viewport and only
topContentplus the preview scroll. - When the panel alone is taller than the viewport, the complete menu scrolls as a fallback so every action remains reachable.
ContextMenuRegion(
overflowStrategy: ContextMenuOverflowStrategy.pinActions,
actions: actions,
child: child,
)
Available strategies:
pinActions: keeps actions fixed when possible and scrolls the preview area.scrollAll: scrolls the complete natural-height menu as one block.
repositionThenScroll and scrollPanel remain as deprecated aliases for
pinActions. The deprecated maxPanelHeightFactor and
ContextMenuPanel.maxHeight parameters are retained only for source
compatibility and no longer constrain the panel.
Viewport and device padding
The overlay uses MediaQuery.paddingOf(context).top and .bottom as vertical
positioning limits without wrapping its content in SafeArea. This keeps menus
away from status, camera, and gesture areas while allowing horizontal layout
and entrance motion to use the complete device viewport. Final clipping occurs
at the overlay's full-screen boundary.
Animation
The preview, top content, and action panel use one shared transition. Centered
menus grow in place, left-aligned menus enter from left to right, and
right-aligned menus use the exact mirrored motion from right to left. This
keeps every part synchronized and works consistently inside an OverlayEntry.
ContextMenuRegion(
preview: customPreview,
dismissKeyboardOnOpen: true,
restoreFocusOnClose: true,
actions: actions,
child: child,
)
Notes
The package only renders the interaction. It intentionally does not perform app actions such as replying, copying, reporting, deleting, or mutating data. Those side effects belong to the app through each action’s onPressed.
ContextMenuActionData does not require an identifier. If you need analytics or routing, keep that mapping in your app layer and call it from onPressed.
Example
The package includes a runnable conversation example with automatic alignment, custom previews, top content, standard actions, destructive styling, long press, and secondary click:
cd example
flutter run
Libraries
- Preview-based context menus for touch, mouse, and trackpad interactions.