flex_overlay 0.6.1+2
flex_overlay: ^0.6.1+2 copied to clipboard
A pure positioning system for Flutter popups, tooltips, and overlays. Smart alignment-based positioning with automatic edge detection and fallback strategies, without imposing any styling.
flex overlay — pure positioning for Flutter popups & tooltips
Smart positioning system without imposed styling—complete control over appearance
Features #
- Pure Positioning - No styling imposed, you control 100% of the appearance
- Smart Alignment - Alignment-based positioning with automatic fallback strategies
- Dual Interaction Modes - Click or hover interactions
- Programmatic Control - Show/hide via external state
- Edge-Aware - Automatically keeps popups within screen bounds
- Fully Customizable - Gap, offset, edge margins all configurable
- Dynamic Content - Handles content size changes gracefully
- Scoped Boundaries - Constrain popups to specific regions
- Auto-Hide - Optional timeout for automatic dismissal
- Zero Dependencies - Pure Flutter, no extra packages
What Makes FlexOverlay Special? #
Unlike basic tooltip libraries, FlexOverlay includes intelligent positioning that handles edge cases automatically:
Smart Fallback System Try to show a tooltip on top, but the trigger is near the screen edge? FlexOverlay automatically tries alternative positions:
- Preferred position (e.g., top)
- Opposite direction (e.g., bottom)
- Perpendicular alternatives (e.g., left, right)
- Uses the best fit without manual intervention
Overlap Prevention Popups never cover the trigger widget. This is especially important for hover interactions where covering the trigger would cause flickering or break the interaction.
Edge Detection Automatically keeps popups within screen bounds with configurable margins. No popups cut off by screen edges or extending beyond the viewport.
All Without Imposed Styling You get smart positioning while maintaining 100% control over appearance. FlexOverlay handles the "where" so you can focus on the "what."
Demo #
Quick Start #
import 'package:flex_overlay/flex_overlay.dart';
Minimal Example #
The simplest tooltip using all defaults:
FlexOverlay(
content: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
),
child: Text('Simple tooltip!', style: TextStyle(color: Colors.white)),
),
child: (_) => Text('Hover me'), // Ignore active state if not needed
)
Basic Tooltip with Custom Position #
FlexOverlay(
positionConfig: PositionConfig.bottom(), // Show below trigger
content: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade700,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
child: Text('Bottom tooltip', style: TextStyle(color: Colors.white)),
),
child: (isActive) => ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: isActive ? Colors.blue.shade700 : Colors.blue,
),
child: Text('Click me'),
),
)
Core Concepts #
1. Interaction Modes #
Click Mode (default)
FlexOverlay(
interactionConfig: InteractionConfig(mode: InteractionMode.click),
content: YourPopup(),
child: (isActive) => YourTrigger(isActive),
)
Hover Mode
FlexOverlay(
interactionConfig: InteractionConfig(
mode: InteractionMode.hover,
hoverShowDelay: Duration(milliseconds: 120),
hoverHideDelay: Duration(milliseconds: 120),
),
content: YourPopup(),
child: (isActive) => YourTrigger(isActive),
)
2. Positioning #
Preset Positions
// Top, Bottom, Left, Right
PositionConfig.top()
PositionConfig.bottom()
PositionConfig.left()
PositionConfig.right()
Custom Alignment-Based Positioning
PositionConfig(
targetAlignment: Alignment.topCenter, // Point on trigger
followerAlignment: Alignment.bottomCenter, // Point on popup
gap: 8.0, // Space between trigger and popup
edgeMargin: 16.0, // Minimum margin from screen edges
offset: Offset(0, 5), // Additional manual offset
)
Examples
// Popup above trigger, aligned to left edges
PositionConfig(
targetAlignment: Alignment.topLeft,
followerAlignment: Alignment.bottomLeft,
gap: 8.0,
)
// Popup to the right, aligned at centers
PositionConfig(
targetAlignment: Alignment.centerRight,
followerAlignment: Alignment.centerLeft,
gap: 12.0,
)
3. Programmatic Control #
Control visibility externally using the visible
property:
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _showTooltip = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () => setState(() => _showTooltip = !_showTooltip),
child: Text('Toggle'),
),
FlexOverlay(
visible: _showTooltip, // External control
content: YourPopup(),
child: (_) => YourTrigger(),
),
],
);
}
}
4. Child Builder #
The child
builder receives the active state - use it if needed, or ignore it:
With active state styling:
FlexOverlay(
content: YourPopup(),
child: (isActive) => Container(
decoration: BoxDecoration(
color: isActive ? Colors.blue.shade700 : Colors.blue,
),
child: Text('Styled on active'),
),
)
Without active state (just ignore the parameter):
FlexOverlay(
content: YourPopup(),
child: (_) => Icon(Icons.info_outline),
)
Advanced Features #
Auto-Hide Timeout #
Automatically hide popup after a duration:
FlexOverlay(
interactionConfig: InteractionConfig(
mode: InteractionMode.click,
autoHideTimeout: Duration(seconds: 3), // Auto-hide after 3s
),
content: YourPopup(),
child: (isActive) => YourTrigger(isActive),
)
Scoped Boundaries #
Constrain popups to stay within a specific region:
FlexOverlayScope(
child: Sidebar(
child: Column(
children: [
FlexOverlay(
// This popup will stay within the sidebar bounds
content: YourPopup(),
child: (isActive) => YourTrigger(isActive),
),
],
),
),
)
Perfect for:
- Sidebars
- Dialogs
- Panels
- Scrollable regions
- Split-pane layouts
Fine-Tuning Position #
FlexOverlay(
positionConfig: PositionConfig(
targetAlignment: Alignment.topCenter,
followerAlignment: Alignment.bottomCenter,
gap: 12.0, // Space between trigger and popup
edgeMargin: 20.0, // Minimum distance from screen edges
offset: Offset(5, -3), // Additional manual tweaking
),
content: YourPopup(),
child: (isActive) => YourTrigger(isActive),
)
Rich Content Popups #
FlexOverlay doesn't impose styling, so you can create any design:
FlexOverlay(
content: Container(
constraints: BoxConstraints(maxWidth: 350),
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple.shade50, Colors.blue.shade50],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 24,
offset: Offset(0, 12),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(Icons.lightbulb, color: Colors.amber),
SizedBox(width: 12),
Text('Pro Tip', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
SizedBox(height: 12),
Text('You have complete control over styling!'),
SizedBox(height: 16),
ElevatedButton(onPressed: () {}, child: Text('Learn More')),
],
),
),
child: (isActive) => YourTrigger(isActive),
)
Common Use Cases #
Tooltip #
FlexOverlay(
interactionConfig: InteractionConfig(mode: InteractionMode.hover),
positionConfig: PositionConfig.top(),
content: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(4),
),
child: Text('Tooltip text', style: TextStyle(color: Colors.white)),
),
child: (_) => Icon(Icons.help_outline),
)
Dropdown Menu #
FlexOverlay(
interactionConfig: InteractionConfig(mode: InteractionMode.click),
positionConfig: PositionConfig.bottom(),
content: Container(
width: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(blurRadius: 8, color: Colors.black26)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(title: Text('Option 1'), onTap: () {}),
ListTile(title: Text('Option 2'), onTap: () {}),
ListTile(title: Text('Option 3'), onTap: () {}),
],
),
),
child: (isActive) => TextButton(
onPressed: () {},
child: Row(
children: [
Text('Menu'),
Icon(isActive ? Icons.arrow_drop_up : Icons.arrow_drop_down),
],
),
),
)
Context Menu #
FlexOverlay(
interactionConfig: InteractionConfig(mode: InteractionMode.click),
positionConfig: PositionConfig(
targetAlignment: Alignment.bottomRight,
followerAlignment: Alignment.topRight,
),
content: YourContextMenu(),
child: (isActive) => IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {},
),
)
Popover Card #
FlexOverlay(
interactionConfig: InteractionConfig(mode: InteractionMode.click),
positionConfig: PositionConfig.bottom(),
content: Card(
elevation: 8,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('User Profile', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('email@example.com'),
SizedBox(height: 12),
ElevatedButton(onPressed: () {}, child: Text('View Profile')),
],
),
),
),
child: (isActive) => CircleAvatar(child: Icon(Icons.person)),
)
API Reference #
FlexOverlay #
Parameter | Type | Default | Description |
---|---|---|---|
child |
Widget Function(bool isActive) |
required | Builder for trigger widget that receives active state. Ignore the parameter if you don't need it. |
content |
Widget |
required | The popup content to display |
positionConfig |
PositionConfig |
PositionConfig.top() |
Position configuration |
interactionConfig |
InteractionConfig |
InteractionConfig() |
Interaction behavior configuration |
visible |
bool? |
null |
Override for programmatic control |
PositionConfig #
Parameter | Type | Default | Description |
---|---|---|---|
targetAlignment |
Alignment |
required | Point on trigger widget |
followerAlignment |
Alignment |
required | Point on popup widget |
gap |
double |
8.0 |
Space between trigger and popup |
edgeMargin |
double |
16.0 |
Minimum margin from screen edges |
offset |
Offset? |
null |
Additional manual offset |
Presets: .top()
, .bottom()
, .left()
, .right()
InteractionConfig #
Parameter | Type | Default | Description |
---|---|---|---|
mode |
InteractionMode |
InteractionMode.click |
Click or hover interaction |
hoverShowDelay |
Duration |
120ms |
Delay before showing on hover |
hoverHideDelay |
Duration |
120ms |
Delay before hiding after hover exit |
autoHideTimeout |
Duration? |
null |
Auto-hide after duration |
FlexOverlayScope #
Parameter | Type | Description |
---|---|---|
child |
Widget |
Widget tree defining the constrained region |
Tips & Best Practices #
- Use alignment-based positioning for stable popups that don't jump when content size changes
- Set appropriate edge margins to ensure popups don't touch screen edges
- Use FlexOverlayScope when working with sidebars, panels, or split layouts
- Leverage programmatic control for complex UI flows (wizards, tours, etc.)
- Keep hover delays reasonable (100-200ms) for good UX
- Style your popups consistently across your app for a cohesive experience
Example App #
Run the example app to see all features in action:
cd example
flutter run
The example demonstrates:
- All positioning modes
- Click vs hover interactions
- Programmatic control
- Edge constraint handling
- Dynamic content
- Fine-tuning parameters
- Scoped boundaries
- Rich content styling
License #
MIT License - see LICENSE file for details.
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.