image

11 Jan 2026

9K

35K

Creating Dynamic Notification Toast Widgets in Flutter

Introduction

In modern mobile applications, providing immediate feedback to users is crucial for a smooth and intuitive experience. Notification toast widgets are a popular UI pattern used to display brief, non-intrusive messages, such as "Item added to cart," "Login successful," or "Network error." While Flutter offers robust ways to build UIs, creating a truly dynamic and reusable notification toast system requires a good understanding of its overlay capabilities and state management.

This article will guide you through the process of building a dynamic notification toast widget in Flutter. We'll cover everything from the basic widget structure and utilizing Flutter's overlay system to adding animations and implementing a service to manage multiple toasts effectively from anywhere in your application.

Understanding Flutter's Overlay System

Flutter's

Overlay
and
OverlayEntry
widgets are fundamental to creating elements that float above the normal widget tree, such as dialogs, tooltips, and, of course, toast notifications. An
Overlay
is a widget that maintains a stack of
OverlayEntry
s. Each
OverlayEntry
can display a widget and can be inserted or removed from the overlay stack.

This system allows our toast notifications to appear on top of any other widgets in the application without interfering with the existing widget layout or requiring us to pass contexts deep down the widget tree.

Designing the Notification Toast Widget

First, let's define the visual appearance and dynamic properties of our toast notification. We'll want to display different types of messages (e.g., success, error, info) and customize their appearance accordingly.

We'll start by defining an enum for notification types and a simple

StatelessWidget
for the toast itself.


import 'package:flutter/material.dart';

enum NotificationType {
  success,
  error,
  info,
}

class NotificationToast extends StatelessWidget {
  final String message;
  final NotificationType type;

  const NotificationToast({
    Key? key,
    required this.message,
    this.type = NotificationType.info,
  }) : super(key: key);

  Color _getBackgroundColor() {
    switch (type) {
      case NotificationType.success:
        return Colors.green;
      case NotificationType.error:
        return Colors.red;
      case NotificationType.info:
        return Colors.blue;
    }
  }

  IconData _getIconData() {
    switch (type) {
      case NotificationType.success:
        return Icons.check_circle_outline;
      case NotificationType.error:
        return Icons.error_outline;
      case NotificationType.info:
        return Icons.info_outline;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent, // Important for showing the curved corners
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: _getBackgroundColor(),
          borderRadius: BorderRadius.circular(10),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.2),
              spreadRadius: 2,
              blurRadius: 5,
              offset: const Offset(0, 3),
            ),
          ],
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              _getIconData(),
              color: Colors.white,
            ),
            const SizedBox(width: 10),
            Flexible(
              child: Text(
                message,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 16,
                ),
                overflow: TextOverflow.ellipsis,
                maxLines: 2,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Implementing the Toast Service

To make our toasts truly dynamic and easily accessible throughout the app, we'll create a

ToastService
. This service will be responsible for managing the
OverlayEntry
, inserting and removing toasts, and handling their display duration.

We'll use a singleton pattern for the service, allowing us to call its methods from any part of the widget tree without needing to pass a

BuildContext
around constantly.


import 'dart:async';
import 'package:flutter/material.dart';

class ToastService {
  static final ToastService _instance = ToastService._internal();

  factory ToastService() {
    return _instance;
  }

  ToastService._internal();

  OverlayEntry? _overlayEntry;
  Timer? _hideTimer;
  BuildContext? _currentContext; // Store context to access OverlayState

  /// Initializes the ToastService with the app's root context.
  /// This should be called once, typically in MyApp's build method or a SplashScreen.
  void init(BuildContext context) {
    _currentContext = context;
  }

  void showToast({
    required String message,
    NotificationType type = NotificationType.info,
    Duration duration = const Duration(seconds: 3),
  }) {
    if (_currentContext == null) {
      debugPrint("ToastService not initialized. Call init() first.");
      return;
    }

    // Dismiss any existing toast before showing a new one
    hideToast();

    _overlayEntry = OverlayEntry(
      builder: (context) => Positioned(
        top: MediaQuery.of(context).padding.top + 10, // Position below status bar
        left: 0,
        right: 0,
        child: Align(
          alignment: Alignment.topCenter,
          child: NotificationToast(
            message: message,
            type: type,
          ),
        ),
      ),
    );

    Overlay.of(_currentContext!).insert(_overlayEntry!);

    _hideTimer = Timer(duration, () {
      hideToast();
    });
  }

  void hideToast() {
    _hideTimer?.cancel();
    if (_overlayEntry != null) {
      _overlayEntry!.remove();
      _overlayEntry = null;
    }
  }
}

To use the

ToastService
, you need to initialize it with a
BuildContext
that can access the global
Overlay
. A good place to do this is in your
MyApp
widget's
build
method or a dedicated loading screen.


// In your main.dart or app entry point
import 'package:flutter/material.dart';
import 'package:your_app_name/toast_service.dart'; // Assuming your service file path

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State createState() => _MyAppState();
}

class _MyAppState extends State {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      // Initialize the ToastService after the first frame is built
      ToastService().init(context);
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dynamic Toast Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Toast Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                ToastService().showToast(
                  message: 'Login successful!',
                  type: NotificationType.success,
                );
              },
              child: const Text('Show Success Toast'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                ToastService().showToast(
                  message: 'Please check your internet connection.',
                  type: NotificationType.error,
                  duration: const Duration(seconds: 5),
                );
              },
              child: const Text('Show Error Toast'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                ToastService().showToast(
                  message: 'A new update is available.',
                  type: NotificationType.info,
                );
              },
              child: const Text('Show Info Toast'),
            ),
          ],
        ),
      ),
    );
  }
}

Adding Animation to Toasts

A static toast appearing instantly can feel jarring. Let's add some smooth slide-in and fade-out animations to our toast notifications. We'll wrap our

NotificationToast
with an
AnimatedBuilder
and use an
AnimationController
.

We'll need to modify the

ToastService
to manage the animation controller.


// Modify ToastService
class ToastService {
  // ... existing fields ...

  AnimationController? _animationController;

  void showToast({
    required String message,
    NotificationType type = NotificationType.info,
    Duration duration = const Duration(seconds: 3),
    Curve enterCurve = Curves.easeOutBack,
    Curve exitCurve = Curves.easeInQuad,
  }) {
    if (_currentContext == null) {
      debugPrint("ToastService not initialized. Call init() first.");
      return;
    }

    hideToast(); // Dismiss any existing toast

    _animationController = AnimationController(
      vsync: Navigator.of(_currentContext!).overlay!, // Use the OverlayState as TickerProvider
      duration: const Duration(milliseconds: 300),
      reverseDuration: const Duration(milliseconds: 200),
    );

    final Animation offsetAnimation = Tween(
      begin: const Offset(0, -1), // Start from above
      end: Offset.zero,
    ).animate(CurvedAnimation(parent: _animationController!, curve: enterCurve));

    final Animation fadeAnimation = Tween(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(parent: _animationController!, curve: Curves.easeIn));

    _overlayEntry = OverlayEntry(
      builder: (context) => Positioned(
        top: MediaQuery.of(context).padding.top + 10,
        left: 0,
        right: 0,
        child: SlideTransition(
          position: offsetAnimation,
          child: FadeTransition(
            opacity: fadeAnimation,
            child: Align(
              alignment: Alignment.topCenter,
              child: NotificationToast(
                message: message,
                type: type,
              ),
            ),
          ),
        ),
      ),
    );

    Overlay.of(_currentContext!).insert(_overlayEntry!);
    _animationController!.forward(); // Start animation to show

    _hideTimer = Timer(duration, () {
      _animationController!.reverse().then((_) { // Animate out, then remove
        hideToast();
      });
    });
  }

  void hideToast() {
    _hideTimer?.cancel();
    if (_overlayEntry != null && _animationController != null) {
      if (_animationController!.isAnimating) {
        // If an animation is ongoing, let it complete reverse
        _animationController!.reverse().then((_) {
          _overlayEntry!.remove();
          _overlayEntry = null;
          _animationController?.dispose();
          _animationController = null;
        });
      } else {
        // If not animating, just remove
        _overlayEntry!.remove();
        _overlayEntry = null;
        _animationController?.dispose();
        _animationController = null;
      }
    } else if (_overlayEntry != null) {
      _overlayEntry!.remove();
      _overlayEntry = null;
    }
  }
}

Note: For

AnimationController
to work, it requires a
TickerProvider
. Since our
ToastService
is not a
StatefulWidget
, we cannot use
SingleTickerProviderStateMixin
directly. A common solution is to pass the
OverlayState
as the
vsync
argument, as shown above, or ensure the service is initialized within a widget that provides a
TickerProvider
.

Managing Multiple Toasts (Optional Advanced)

The current implementation immediately dismisses any active toast when a new one is requested. For some applications, you might want to:

  • Queue Toasts: Show toasts one after another.
  • Stack Toasts: Display multiple toasts simultaneously, usually by positioning them relative to each other.

Implementing a queue would involve using a

Queue
data structure to hold pending toast requests and showing the next one only after the current one has finished its exit animation. Stacking would require more complex
Positioned
logic within the
OverlayEntry
or managing multiple
OverlayEntry
s. For simplicity, we stick to the "one toast at a time" model in the main example.

Best Practices and Considerations

  • Accessibility: For users with visual impairments, a purely visual toast might not be sufficient. Consider integrating with accessibility services (e.g., using
    Semantics
    or announcing messages via screen readers).
  • Context Management: While our
    ToastService
    uses a stored
    BuildContext
    , be mindful of how you initialize it. The context must be one that contains an
    Overlay
    widget (like the one provided by
    MaterialApp
    or
    WidgetsApp
    ).
  • Disposing Resources: Always ensure that
    AnimationController
    s and
    OverlayEntry
    s are properly disposed of to prevent memory leaks, especially when the toast is dismissed. Our
    hideToast
    method handles this.
  • Theming: Extend your
    NotificationToast
    widget to use
    Theme.of(context)
    for colors, fonts, and other stylistic properties to ensure consistency with your app's overall theme.
  • User Interaction: Depending on your needs, you might want to add dismiss gestures (like swiping) or tap functionality to the toast.

Conclusion

By leveraging Flutter's powerful overlay system and implementing a dedicated service, we've successfully created a dynamic, animated, and easily manageable notification toast widget. This pattern allows for consistent user feedback across your application without tightly coupling your UI components to toast display logic. With this foundation, you can further customize the appearance, behavior, and complexity of your toast notifications to perfectly fit your application's needs.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is