image

09 Dec 2025

9K

35K

Creating Custom Dialogs & Alerts in Flutter

Dialogs and alerts are essential UI components for user interaction in any mobile application. They provide a way to prompt users for information, display crucial messages, or confirm actions without navigating away from the current screen. While Flutter offers robust built-in solutions like AlertDialog and SimpleDialog, applications often require highly customized dialogs to match specific branding, complex layouts, or unique user experiences.

Understanding Flutter's Built-in Dialogs

Flutter provides two primary Material Design dialogs out of the box:

  • AlertDialog: Ideal for interrupting users with urgent information, requiring decisions, or confirming actions. It typically includes a title, content, and an array of actions (buttons).
  • SimpleDialog: Used for presenting a list of choices to the user. It usually consists of a title and a list of SimpleDialogOption widgets.

Here's a quick example of a standard AlertDialog:


Future<void> _showAlertDialog(BuildContext context) async {
  return showDialog<void>(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: const Text('Discard Changes?'),
        content: const SingleChildScrollView(
          child: ListBody(
            children: <Widget>[
              Text('You have unsaved changes.'),
              Text('Would you like to discard them?'),
            ],
          ),
        ),
        actions: <Widget>[
          TextButton(
            child: const Text('Cancel'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
          TextButton(
            child: const Text('Discard'),
            onPressed: () {
              Navigator.of(context).pop(); // You might pass true/false here
            },
          ),
        ],
      );
    },
  );
}

Why Custom Dialogs?

While the built-in dialogs are convenient, they have limitations when you need:

  • Unique UI/UX Designs: To precisely match a custom design system that deviates from Material Design.
  • Complex Layouts: Dialogs containing forms, image carousels, maps, or other intricate widgets.
  • Custom Animations: Specific entry/exit animations not achievable with standard dialogs.
  • Interactive Elements: Custom input fields, sliders, or other interactive components within the dialog body.
  • Non-modal Behavior: Although less common for "dialogs," sometimes you might want overlay-like elements that aren't strict modals.

The Foundation: showDialog

The core function for displaying any dialog in Flutter, custom or otherwise, is showDialog. It's a top-level function that pushes a route (a dialog route) onto the navigator, displaying the dialog above the current contents of the app.

The showDialog function takes a BuildContext and a builder function as its primary arguments. The builder function is responsible for returning the widget that will serve as your dialog.


Future<T?> showDialog<T>({
  required BuildContext context,
  required WidgetBuilder builder,
  bool barrierDismissible = true,
  Color? barrierColor = Colors.black54,
  String? barrierLabel,
  bool useSafeArea = true,
  bool useRootNavigator = true,
  RouteSettings? routeSettings,
  Offset? anchorPoint,
})

The key here is the builder. Whatever widget you return from the builder will be enclosed within a Material Design dialog theme and presented to the user. This is where your custom dialog widget comes in.

Building a Custom Dialog Widget

To create a custom dialog, you typically define a new StatelessWidget or StatefulWidget that encapsulates its entire UI and logic. This widget will then be returned by the builder function of showDialog.

Let's create a simple custom confirmation dialog that looks a bit different from the standard AlertDialog.


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

class CustomConfirmDialog extends StatelessWidget {
  final String title;
  final String content;
  final String confirmButtonText;
  final String cancelButtonText;

  const CustomConfirmDialog({
    Key? key,
    required this.title,
    required this.content,
    this.confirmButtonText = 'Yes',
    this.cancelButtonText = 'No',
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Dialog( // Or AlertDialog for more default styling
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
      elevation: 0,
      backgroundColor: Colors.transparent,
      child: _dialogContent(context),
    );
  }

  Widget _dialogContent(BuildContext context) {
    return Stack(
      children: <Widget>[
        Container(
          padding: const EdgeInsets.only(
            top: 60,
            bottom: 16,
            left: 16,
            right: 16,
          ),
          margin: const EdgeInsets.only(top: 45),
          decoration: BoxDecoration(
            color: Colors.white,
            shape: BoxShape.rectangle,
            borderRadius: BorderRadius.circular(16),
            boxShadow: const [
              BoxShadow(
                color: Colors.black26,
                blurRadius: 10.0,
                offset: Offset(0.0, 10.0),
              ),
            ],
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min, // To make the dialog compact
            children: <Widget>[
              Text(
                title,
                style: const TextStyle(
                  fontSize: 24.0,
                  fontWeight: FontWeight.w700,
                ),
              ),
              const SizedBox(height: 16.0),
              Text(
                content,
                textAlign: TextAlign.center,
                style: const TextStyle(
                  fontSize: 16.0,
                ),
              ),
              const SizedBox(height: 24.0),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {
                        Navigator.of(context).pop(false); // User canceled
                      },
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.grey[300],
                        foregroundColor: Colors.black,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8.0),
                        ),
                      ),
                      child: Text(cancelButtonText),
                    ),
                  ),
                  const SizedBox(width: 16.0),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {
                        Navigator.of(context).pop(true); // User confirmed
                      },
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Theme.of(context).primaryColor,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8.0),
                        ),
                      ),
                      child: Text(confirmButtonText),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
        Positioned(
          left: 16,
          right: 16,
          child: CircleAvatar(
            backgroundColor: Theme.of(context).primaryColor,
            radius: 45,
            child: const Icon(Icons.help_outline, color: Colors.white, size: 50),
          ),
        ),
      ],
    );
  }
}

In this example:

  • We use a Dialog widget as the root of our custom dialog. It gives us more control over its shape, elevation, and background compared to directly using an AlertDialog.
  • _dialogContent builds the actual layout, using a Stack to overlay a circular icon at the top, mimicking a common custom alert design.
  • Navigator.of(context).pop(true) or (false) is used to close the dialog and return a boolean value, indicating the user's choice.

Integrating the Custom Dialog

To display your CustomConfirmDialog, you simply pass it to the builder of showDialog:


// In your widget where you want to show the dialog
ElevatedButton(
  child: const Text('Show Custom Confirm'),
  onPressed: () async {
    final bool? confirmed = await showDialog<bool>(
      context: context,
      barrierDismissible: false, // User must tap a button to close
      builder: (BuildContext context) {
        return const CustomConfirmDialog(
          title: 'Confirm Action',
          content: 'Are you sure you want to proceed with this action? This cannot be undone.',
        );
      },
    );

    if (confirmed != null && confirmed) {
      // User tapped 'Yes'
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Action Confirmed!')),
      );
    } else {
      // User tapped 'No' or dismissed (if barrierDismissible was true)
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Action Canceled.')),
      );
    }
  },
),

Notice how showDialog returns a Future<T?>. By awaiting this future, you can capture the value (in this case, a boolean) that was passed to Navigator.of(context).pop(), allowing you to react to the user's choice.

Creating a Custom Alert (e.g., Success/Error)

Often, you need a transient alert, like a success or error message, that doesn't necessarily require user interaction to dismiss, or presents a more visually distinct look. While SnackBar is great for this, sometimes a full-modal custom alert is preferred.

Let's create a custom "Success" alert that appears as a modal dialog and automatically dismisses after a few seconds or when the user taps 'OK'.


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

class CustomSuccessAlert extends StatelessWidget {
  final String message;
  final VoidCallback? onOkPressed;

  const CustomSuccessAlert({
    Key? key,
    required this.message,
    this.onOkPressed,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Dialog(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
      elevation: 0,
      backgroundColor: Colors.transparent,
      child: _alertContent(context),
    );
  }

  Widget _alertContent(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16.0),
        boxShadow: const [
          BoxShadow(
            color: Colors.black26,
            blurRadius: 10.0,
            offset: Offset(0.0, 10.0),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          const CircleAvatar(
            backgroundColor: Colors.green,
            radius: 30,
            child: Icon(Icons.check, color: Colors.white, size: 40),
          ),
          const SizedBox(height: 16.0),
          const Text(
            'Success!',
            style: TextStyle(
              fontSize: 22.0,
              fontWeight: FontWeight.w700,
            ),
          ),
          const SizedBox(height: 8.0),
          Text(
            message,
            textAlign: TextAlign.center,
            style: const TextStyle(
              fontSize: 16.0,
            ),
          ),
          const SizedBox(height: 24.0),
          Align(
            alignment: Alignment.bottomRight,
            child: TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                onOkPressed?.call();
              },
              child: const Text('OK'),
            ),
          ),
        ],
      ),
    );
  }
}

To show this custom alert:


// In your widget
ElevatedButton(
  child: const Text('Show Custom Success Alert'),
  onPressed: () {
    showDialog(
      context: context,
      barrierDismissible: false, // Can't dismiss by tapping outside
      builder: (BuildContext context) {
        return CustomSuccessAlert(
          message: 'Your data has been saved successfully!',
          onOkPressed: () {
            // Optional: Do something after 'OK' is pressed and dialog is closed
            print('Success alert dismissed via OK button.');
          },
        );
      },
    );

    // Optional: Auto-dismiss after a few seconds
    Future.delayed(const Duration(seconds: 3), () {
      if (Navigator.of(context).canPop()) {
        Navigator.of(context).pop();
      }
    });
  },
),

Best Practices and Considerations

  • useRootNavigator: Set this to true (the default) if you want the dialog to appear above all navigators, including those nested within your app (e.g., if you have a BottomNavigationBar and want the dialog to cover it). Set to false if the dialog should only appear above the current navigator's content.
  • barrierDismissible: Controls whether tapping outside the dialog will dismiss it. Defaults to true. For critical actions, set to false to force a user decision.
  • Accessibility: Ensure your custom dialogs are accessible. Use semantic labels for interactive elements and consider how screen readers will interpret your layout.
  • Responsiveness: Design your dialogs to look good on various screen sizes and orientations. Use FractionallySizedBox or ConstrainedBox to manage the dialog's width/height relative to the screen.
  • Keep it Concise: Dialogs should be short and to the point. Avoid cramming too much information or too many actions into a single dialog.
  • Animations: For more complex animations, you can wrap your custom dialog widget within Flutter's AnimatedBuilder or use a custom PageRouteBuilder with showGeneralDialog instead of showDialog.

Conclusion

Flutter's flexible widget tree and the showDialog function empower developers to create highly customized dialogs and alerts that perfectly align with an application's unique design and functional requirements. By understanding the core principles and leveraging Flutter's powerful layout widgets, you can build engaging and intuitive interactive experiences for your users.

Related Articles

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera

Dec 19, 2025

Building a Countdown Timer Widget in Flutter

Building a Countdown Timer Widget in Flutter Countdown timers are a fundamental component in many modern applications, ranging from e-commerce platforms indica