Every Flutter app needs notifications. Whether it's a success alert after a payment, an error toast when upload fails, or a warning banner before a session expires — you need them everywhere.
Most developers reach for ScaffoldMessenger.showSnackBar() and call it a day. But after building several production apps, I found this approach too limiting. So I built a proper reusable notification system from scratch. Here's everything I learned.
The Problem with ScaffoldMessenger
ScaffoldMessenger works for basic cases but has real limitations:
Tied to Scaffold context — breaks in certain widget trees
Very limited customization (position, style, animations)
Can't easily show dialogs AND toasts simultaneously
No built-in dark mode support
Animations are not customizable
The Solution — Flutter's Overlay API
Overlay is Flutter's built-in system for showing widgets above everything else — it's literally how tooltips, dropdowns, and route transitions work internally.
Here's the core pattern:
dartvoid showCustomToast(BuildContext context) {
final overlay = Overlay.of(context);
late OverlayEntry entry;
entry = OverlayEntry(
builder: (_) => Positioned(
bottom: 40,
left: 16,
right: 16,
child: YourToastWidget(
onDismiss: () => entry.remove(),
),
),
);
overlay.insert(entry);
// Auto dismiss after 3 seconds
Future.delayed(const Duration(seconds: 3), () {
entry.remove();
});
}
Key advantage: this works from anywhere in your widget tree — no Scaffold required.
- Building the Toast A good toast needs:
Smooth entrance/exit animation
Auto dismiss with manual dismiss option
Dark mode awareness
dartclass ToastWidget extends StatefulWidget {
final String message;
final String? title;
final VoidCallback onDismiss;
const ToastWidget({
super.key,
required this.message,
this.title,
required this.onDismiss,
});
@override
State createState() => _ToastWidgetState();
}
class _ToastWidgetState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _slide;
late Animation _opacity;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 380),
);
// Slide up from bottom
_slide = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut, // 👈 this creates the satisfying bounce
));
_opacity = Tween<double>(begin: 0, end: 1)
.animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward();
// Auto dismiss
Future.delayed(const Duration(seconds: 3), _dismiss);
}
void _dismiss() async {
if (!mounted) return;
await _controller.reverse();
widget.onDismiss();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slide,
child: FadeTransition(
opacity: _opacity,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
child: Text(widget.message),
),
),
),
);
}
}
- Dark Mode — Auto Detected, Zero Extra Code The cleanest approach reads directly from Flutter's theme: dartbool isDark = Theme.of(context).brightness == Brightness.dark;
Color surface = isDark
? const Color(0xFF1C1C1E) // dark surface
: Colors.white; // light surface
Color textPrimary = isDark
? const Color(0xFFF9FAFB) // near white
: const Color(0xFF1A1A1A); // near black
Color textSecondary = isDark
? const Color(0xFF9CA3AF) // muted light
: const Color(0xFF6B7280); // muted dark
Now your notification automatically adapts when the user toggles dark mode — no params needed.
For alert type colors, define light/dark pairs:
dart// Success colors
Color successLight = const Color(0xFF1DB954);
Color successDark = const Color(0xFF4ADE80);
Color successBgLight = const Color(0xFFE8FAF0);
Color successBgDark = const Color(0xFF052E16);
Color accent = isDark ? successDark : successLight;
Color iconBg = isDark ? successBgDark : successBgLight;
Animation Styles
One system, five different feels. Here's how to implement each:
Bounce (Most satisfying)
dartScaleTransition(
scale: Tween(begin: 0.6, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.elasticOut, // 👈 magic curve
),
),
child: FadeTransition(opacity: animation, child: child),
)
Flip (3D card effect)
dartAnimatedBuilder(
animation: controller,
builder: (, c) => Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001) // perspective
..rotateX((1 - controller.value) * 3.14159 / 2),
child: FadeTransition(opacity: animation, child: c),
),
child: child,
)
Blur (Soft focus-in)
dart// Use ImageFiltered for blur effect during transition
AnimatedBuilder(
animation: controller,
builder: (, child) {
double blurAmount = (1 - controller.value) * 10;
return ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: blurAmount,
sigmaY: blurAmount,
),
child: Opacity(opacity: controller.value, child: child),
);
},
child: child,
)
Slide
dartSlideTransition(
position: Tween(
begin: const Offset(0, -0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
)),
child: FadeTransition(opacity: animation, child: child),
)Bottom Sheet Alerts
The key insight for bottom sheets is safe area padding. Without it, content gets hidden behind the home bar on modern phones:
dartWidget build(BuildContext context) {
// 👇 ALWAYS get this
final bottomPad = MediaQuery.of(context).padding.bottom;
return Container(
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
),
child: Padding(
padding: EdgeInsets.only(
left: 24,
right: 24,
top: 20,
bottom: bottomPad + 16, // 👈 safe area aware
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handle
Container(
width: 36, height: 4,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
// your content
],
),
),
);
}
For destructive actions, use a subtle red background instead of a filled red button — it's less alarming but still communicates danger:
dart// ✅ Better destructive button
Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
// subtle red bg instead of solid red
color: Colors.red.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Delete Account',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.w700,
),
),
)
- The Full Architecture Here's how I structured it into a reusable system: lib/ └── notifications/ ├── nk_type.dart ← NotifType enum + color configs ├── nk_theme.dart ← Global theme (borderRadius, font, duration) ├── nk_dialog.dart ← Popup alerts ├── nk_toast.dart ← Floating toasts ├── nk_banner.dart ← Full-width banners └── nk_sheet.dart ← Bottom sheet alerts Each component follows the same pattern: dartclass MyToast { static OverlayEntry? _current;
static void show(BuildContext context, {required String message}) {
// Remove existing toast first
_current?.remove();
final overlay = Overlay.of(context);
late OverlayEntry entry;
entry = OverlayEntry(
builder: (_) => ToastWidget(
message: message,
onDismiss: () {
entry.remove();
if (_current == entry) _current = null;
},
),
);
_current = entry;
overlay.insert(entry);
}
static void dismiss() {
_current?.remove();
_current = null;
}
}
This static _current pattern ensures only one toast shows at a time — the new one always replaces the old one.
Key Takeaways
Use Overlay instead of ScaffoldMessenger for full customization
Read brightness from Theme for automatic dark mode — no extra params
Curves.elasticOut makes bounce animations feel natural and satisfying
Always add MediaQuery bottom padding in bottom sheets for safe area
Matrix4 perspective (setEntry(3, 2, 0.001)) is the key to 3D flip effects
Static OverlayEntry lets you manage dismiss/replace behavior cleanly
What I Packaged This Into
I turned this entire system into NotiX Pro — a Flutter package with all 4 components, dark mode, 5 animation styles, and a commercial license.
Early bird pricing at $2 👉 kamaliv.gumroad.com/l/siarxs
A free version (notify_kit) is also on pub.dev with the basic components.
Have questions about the implementation? Drop them in the comments — I'll reply to every one! 👇
What notification patterns do you use in your Flutter apps?
Top comments (0)