flutter_anchor 0.0.12
flutter_anchor: ^0.0.12 copied to clipboard
A Flutter package for dynamic anchored overlays, including tooltips, popovers, and more
flutter_anchor #
Core Flutter widgets for the Anchor overlay system. This package provides the foundational widgets (Anchor, RawAnchor) for connecting a floating overlay to an anchor widget.
Note: This package provides the core functionality (triggers, positioning) but no default styling. If you are looking for common pre-styled containers, like tooltips with arrows, see the anchor_ui package.
What's Included #
Anchor: A high-level widget that includes built-in trigger logic (tap, hover, focus, manual).RawAnchor: A low-level widget that only handles positioning. It gives you full control over the overlay state and requires you to provide your own positioning middleware explicitly.AnchorContextMenu: A solution for showing overlays at a virtual "cursor" position.
Usage #
1. Full Control (Anchor) #
The base Anchor widget gives you full control over triggers and appearance.
Focus Trigger (e.g., Search Bar)
Use AnchorTriggerMode.focus to show an overlay when a TextField is focused.
final _focusNode = FocusNode();
Anchor(
triggerMode: AnchorTriggerMode.focus(focusNode: _focusNode),
placement: Placement.bottom,
// The overlay doesn't have any default styling
overlayBuilder: (context) {
return Container(
width: 300,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: ListView(
children: const [
ListTile(title: Text('Suggestion 1')),
ListTile(title: Text('Suggestion 2')),
],
),
);
},
child: TextField(
focusNode: _focusNode,
decoration: const InputDecoration(hintText: 'Search...'),
),
)
Manual Trigger (e.g., Menu Bar)
Use an AnchorController to show/hide overlays programmatically.
final _controller = AnchorController();
Anchor(
controller: _controller,
triggerMode: const AnchorTriggerMode.manual(),
placement: Placement.bottomStart,
overlayBuilder: (context) => _buildMenuContent(),
child: IconButton(
icon: const Icon(Icons.menu),
// Toggle the menu on button press
onPressed: () => _controller.toggle(),
),
)
Advanced Customization #
Custom Positioning (RawAnchor) #
While Anchor handles common cases, you can use RawAnchor to compose a custom positioning pipeline by explicitly passing middlewares.
final _controller = AnchorController();
RawAnchor(
// A controller to manage the overlay state
controller: _controller,
// This is the initial, preferred placement
placement: Placement.top,
// Define custom positioning middleware
middlewares: const [
// 1. Add a 10px gap between the child and overlay
OffsetMiddleware(mainAxis: OffsetValue.value(10)),
// 2. If it overflows, try the opposite side
FlipMiddleware(),
// 3. If it still overflows (e.g., on the sides), shift it
ShiftMiddleware(),
],
overlayBuilder: (context) {
return Container(
padding: const EdgeInsets.all(8),
color: Colors.black,
child: const Text('My Overlay', style: TextStyle(color: Colors.white)),
);
},
child: ElevatedButton(
onPressed: () {},
child: const Text('Tap Me'),
),
)
Avoiding Keyboard and System UI
When using Anchor with text fields, you'll often want the overlay to avoid the on-screen keyboard and system UI elements (status bar, navigation bar). Use the viewPadding parameter to define safe areas.
Important: Due to how Flutter's OverlayPortal works, overlays are rendered in a separate layer that doesn't receive keyboard insets from MediaQuery. You must read these values before the overlay layer and pass them explicitly.
@override
Widget build(BuildContext context) {
// Read MediaQuery values at the Scaffold level (before entering OverlayPortal)
final viewPadding = MediaQuery.viewPaddingOf(context);
final viewInsets = MediaQuery.viewInsetsOf(context);
// Combine both to avoid system UI AND keyboard
final padding = viewPadding + viewInsets;
return Scaffold(
body: Center(
child: Anchor(
// Pass the padding to keep overlay away from edges and keyboard
viewPadding: padding,
overlayBuilder: (context) => _buildOverlayContent(),
),
),
);
}
Backdrop #
You can create a backdrop (like a modal barrier or blur filter) by providing a backdropBuilder to Anchor or RawAnchor.
Anchor(
// ... other properties
backdropBuilder: (context) => ClipPath(
// Clip a "hole" where the child widget is
clipper: _BackdropClipper(
exclude: AnchorData.of(context).geometry.childBounds,
excludeRadius: BorderRadius.circular(8),
),
// Apply a blur filter to the entire backdrop
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: Container(color: Colors.black26),
),
),
child: YourGridItem(),
)
Limitations #
Due to how this package works, there are some limitations to be aware of:
-
Overlay Size Measurement: By default, the engine measures the overlay size after it is built. This means that the first time an overlay appears, there will be a frame where the overlay is invisible until its size is measured and positioning is calculated. This can cause a slight delay when the overlay first appears.
- This only becomes a problem when you want to show your overlay immeditely. To mitigate this, you can
- Provide the size of the overlay in advance using the
overlayWidthandoverlayHeightparameters onAnchor/RawAnchor. - Use animated transition (like
FadeTransition) to smooth out the appearance
- Provide the size of the overlay in advance using the
- This only becomes a problem when you want to show your overlay immeditely. To mitigate this, you can
-
Size Middleware: Using
SizeMiddlewarewill causes engine to rerun on every build, which may lead to performance issues. So only use it when you really need dynamic sizing based on available space.
Demos #
| Demo | Description |
|---|---|
| Search Autocomplete | Display a list of suggestions when a TextField gains focus. |
| Chat Reactions | Show an emoji reaction bar on hover, just like in MS Teams or Slack. |