Flutter Overlays: Creating Floating UI Elements with OverlayEntry
Have you ever wanted to create a custom tooltip that appears above other widgets, or build a floating menu that doesn't disrupt your widget tree? Flutter's Overlay system is your answer. While many developers use standard dialogs and tooltips, understanding Overlays opens up a world of possibilities for creating sophisticated floating UI elements.
In this article, we'll explore what Overlays are, how they work, and when to use them. By the end, you'll be able to create custom floating widgets that appear anywhere on your screen, independent of your main widget tree.
What Are Overlays?
An Overlay is a special widget that sits above all other widgets in your app. Think of it as a transparent layer that can hold multiple floating widgets called OverlayEntries. These entries can be positioned anywhere on the screen and are perfect for elements like tooltips, dropdown menus, custom dialogs, or any UI that needs to float above your main content.
The key advantage of Overlays is that they exist outside your normal widget hierarchy. This means you can show a floating widget without restructuring your entire widget tree or worrying about z-index issues.
Understanding OverlayEntry
An OverlayEntry is a single widget that lives inside an Overlay. Each entry has a builder function that returns the widget to display. The magic happens when you insert or remove these entries dynamically.
Here's a basic example of creating and showing an OverlayEntry:
import 'package:flutter/material.dart';
class OverlayExample extends StatefulWidget {
const OverlayExample({super.key});
@override
State<OverlayExample> createState() => _OverlayExampleState();
}
class _OverlayExampleState extends State<OverlayExample> {
OverlayEntry? _overlayEntry;
void _showOverlay() {
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: 100,
left: 100,
child: Material(
color: Colors.transparent,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'Floating Widget!',
style: TextStyle(color: Colors.white),
),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
void _hideOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
void dispose() {
_hideOverlay();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Overlay Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _showOverlay,
child: const Text('Show Overlay'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _hideOverlay,
child: const Text('Hide Overlay'),
),
],
),
),
);
}
}
In this example, we create an OverlayEntry with a builder that returns a Positioned widget. The Positioned widget allows us to place the overlay anywhere on the screen using top, left, right, or bottom properties. When we call Overlay.of(context).insert(), the overlay appears above all other widgets.
Positioning Overlays Relative to Widgets
One of the most common use cases is positioning an overlay relative to another widget, like showing a tooltip above a button. To do this, we need to find the position of the target widget using a GlobalKey and RenderBox.
class TooltipOverlayExample extends StatefulWidget {
const TooltipOverlayExample({super.key});
@override
State<TooltipOverlayExample> createState() => _TooltipOverlayExampleState();
}
class _TooltipOverlayExampleState extends State<TooltipOverlayExample> {
final GlobalKey _buttonKey = GlobalKey();
OverlayEntry? _overlayEntry;
void _showTooltip() {
final RenderBox? renderBox =
_buttonKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final Offset offset = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
top: offset.dy - 50,
width: size.width,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'This is a custom tooltip!',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
void _hideTooltip() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
void dispose() {
_hideTooltip();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tooltip Overlay')),
body: Center(
child: ElevatedButton(
key: _buttonKey,
onPressed: _showTooltip,
onLongPress: _hideTooltip,
child: const Text('Hover for Tooltip'),
),
),
);
}
}
This example demonstrates how to position an overlay relative to a button. We use a GlobalKey to get a reference to the button's RenderBox, which gives us its position and size. Then we calculate where to place the tooltip above the button.
Creating a Reusable Overlay Helper
To make overlays easier to work with, let's create a reusable helper class that handles the complexity of showing and hiding overlays:
class OverlayHelper {
static OverlayEntry? _currentOverlay;
static void showOverlay(
BuildContext context, {
required Widget child,
Offset? position,
Alignment alignment = Alignment.center,
}) {
hideOverlay();
_currentOverlay = OverlayEntry(
builder: (context) => Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: hideOverlay,
child: Container(color: Colors.transparent),
),
),
if (position != null)
Positioned(
left: position.dx,
top: position.dy,
child: child,
)
else
Align(
alignment: alignment,
child: child,
),
],
),
);
Overlay.of(context).insert(_currentOverlay!);
}
static void hideOverlay() {
_currentOverlay?.remove();
_currentOverlay = null;
}
}
This helper class provides a clean API for showing overlays. It automatically handles cleanup and provides options for positioning. The Stack with a transparent GestureDetector allows users to tap outside the overlay to dismiss it, which is a common UX pattern.
Advanced Example: Custom Dropdown Menu
Let's build a more complex example: a custom dropdown menu that appears when you tap a button. This demonstrates how overlays can be used for interactive UI elements:
class CustomDropdownExample extends StatefulWidget {
const CustomDropdownExample({super.key});
@override
State<CustomDropdownExample> createState() => _CustomDropdownExampleState();
}
class _CustomDropdownExampleState extends State<CustomDropdownExample> {
final GlobalKey _buttonKey = GlobalKey();
OverlayEntry? _overlayEntry;
String _selectedItem = 'Select an option';
void _showDropdown() {
if (_overlayEntry != null) {
_hideDropdown();
return;
}
final RenderBox? renderBox =
_buttonKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final Offset offset = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
_overlayEntry = OverlayEntry(
builder: (context) => Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: _hideDropdown,
child: Container(color: Colors.transparent),
),
),
Positioned(
left: offset.dx,
top: offset.dy + size.height + 4,
width: size.width,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildMenuItem('Option 1'),
_buildMenuItem('Option 2'),
_buildMenuItem('Option 3'),
_buildMenuItem('Option 4'),
],
),
),
),
],
),
);
Overlay.of(context).insert(_overlayEntry!);
}
Widget _buildMenuItem(String text) {
return InkWell(
onTap: () {
setState(() {
_selectedItem = text;
});
_hideDropdown();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Text(text),
if (_selectedItem == text) ...[
const Spacer(),
const Icon(Icons.check, size: 16),
],
],
),
),
);
}
void _hideDropdown() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
void dispose() {
_hideDropdown();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Dropdown')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Selected: $_selectedItem'),
const SizedBox(height: 20),
ElevatedButton(
key: _buttonKey,
onPressed: _showDropdown,
child: const Text('Open Dropdown'),
),
],
),
),
);
}
}
This example shows how overlays can be used to create interactive dropdown menus. The overlay appears below the button, and users can select an option or tap outside to dismiss it.
Important Considerations
When working with overlays, keep these points in mind:
Memory Management
Always remove overlay entries when they're no longer needed. Failing to do so can cause memory leaks. The best practice is to remove overlays in the dispose method of your State class, as shown in the examples above.
Context Availability
Overlays require a valid BuildContext. Make sure you're calling Overlay.of(context) from within a widget tree that has access to an Overlay. Most MaterialApp and CupertinoApp widgets provide this automatically.
Performance
Overlays rebuild when their builder functions are called. If your overlay contains expensive widgets, consider using const constructors or caching to optimize performance.
Accessibility
When creating custom overlays, ensure they're accessible. Use Semantics widgets to provide screen reader support, and make sure keyboard navigation works correctly.
When to Use Overlays
Overlays are perfect for:
- Custom tooltips that need precise positioning
- Dropdown menus and popup menus
- Floating action buttons with menus
- Custom modal dialogs
- Toast notifications
- Context menus
- Any UI element that needs to float above other widgets
However, if you can achieve your goal with standard Flutter widgets like Dialog, BottomSheet, or Tooltip, prefer those as they handle edge cases and accessibility automatically.
Conclusion
Flutter's Overlay system is a powerful tool for creating floating UI elements that exist outside your normal widget hierarchy. By understanding how OverlayEntry works and how to position overlays relative to other widgets, you can build sophisticated custom UI components.
Remember to always clean up overlay entries to prevent memory leaks, and consider accessibility when building custom overlays. With practice, you'll find overlays to be an invaluable tool for creating polished, professional Flutter applications.
Happy coding, and may your overlays always appear exactly where you want them!