If you've ever built a complex Flutter app, you've probably run into this annoying UX issue: You open a Dialog, perform an action (like a network request), and trigger a success or error Snackbar. But instead of appearing on top, the Snackbar hides behind the dark overlay of the Dialog!
Here is why this happens and how to fix it permanently.
By default, Flutter developers use this to show Snackbars:
ScaffoldMessenger.of(context).showSnackBar(...);When you open a Dialog using showDialog(), Flutter pushes a new route onto the Navigator. ScaffoldMessenger lives below the Navigator in the rendering tree. Because the Dialog has a barrier color (the dim background), anything drawn by the Scaffold (like your Snackbar) gets covered by that shadow.
To fix this, we completely bypass ScaffoldMessenger and inject our Snackbar directly into the highest possible layer of the app using Flutter's Overlay system.
Instead of calling showSnackBar, we create a helper class that generates an OverlayEntry and inserts it into the rootOverlay.
import 'package:flutter/material.dart';
class GlobalSnackbar {
static OverlayEntry? _currentEntry;
static void show(BuildContext context, String message) {
// 1. Remove the old snackbar if it exists
_currentEntry?.remove();
_currentEntry = null;
// 2. Get the highest overlay in the app
final overlay = Overlay.maybeOf(context, rootOverlay: true);
if (overlay == null) return;
// 3. Create the OverlayEntry
_currentEntry = OverlayEntry(
builder: (context) {
return Positioned(
bottom: 24.0,
left: 24.0,
right: 24.0,
child: Material(
color: Colors.transparent,
child: CustomSnackbarWidget(
message: message,
onDismiss: () {
_currentEntry?.remove();
_currentEntry = null;
},
),
),
);
},
);
// 4. Insert it!
overlay.insert(_currentEntry!);
}
}Because we aren't using the built-in SnackBar, we don't get free animations. We need to create a StatefulWidget that animates its own entrance and handles a dismissal timer.
class CustomSnackbarWidget extends StatefulWidget {
final String message;
final VoidCallback onDismiss;
const CustomSnackbarWidget({
Key? key,
required this.message,
required this.onDismiss,
}) : super(key: key);
@override
State<CustomSnackbarWidget> createState() => _CustomSnackbarWidgetState();
}
class _CustomSnackbarWidgetState extends State<CustomSnackbarWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1), // Start below screen
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
// Animate in
_controller.forward();
// Auto-dismiss after 3 seconds
Future.delayed(const Duration(seconds: 3), dismiss);
}
void dismiss() async {
if (mounted) {
await _controller.reverse(); // Animate out
widget.onDismiss();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
widget.message,
style: const TextStyle(color: Colors.white),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: dismiss,
),
],
),
),
);
}
}Now, anywhere in your app—whether inside a regular screen, a bottom sheet, or a dialog—you just call:
GlobalSnackbar.show(context, 'Data saved successfully!');It will always appear on top of everything else!