← Back to Articles

Flutter Dialogs and Bottom Sheets: Creating Interactive Overlays

Flutter Dialogs and Bottom Sheets: Creating Interactive Overlays

Flutter Dialogs and Bottom Sheets: Creating Interactive Overlays

When building Flutter apps, you often need to grab the user's attention or present important information without navigating away from the current screen. That's where dialogs and bottom sheets come in. These overlay components are essential for creating polished, user-friendly interfaces. In this article, we'll explore how to use dialogs and bottom sheets effectively in your Flutter applications.

Understanding Dialogs

Dialogs are modal overlays that appear on top of your app's content, typically centered on the screen. They're perfect for confirming actions, displaying alerts, or gathering quick input from users. Flutter provides several built-in dialog widgets, each suited for different use cases.

AlertDialog: The Classic Confirmation Dialog

The AlertDialog is the most common dialog type. It's ideal for displaying messages, warnings, or asking for confirmation before performing an action.


import 'package:flutter/material.dart';

void showConfirmationDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: const Text('Delete Item'),
        content: const Text('Are you sure you want to delete this item? This action cannot be undone.'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              // Perform delete action
              Navigator.of(context).pop();
            },
            child: const Text('Delete'),
          ),
        ],
      );
    },
  );
}

The showDialog function takes a BuildContext and a builder function that returns your dialog widget. The Navigator.of(context).pop() method closes the dialog and returns to the previous screen.

AlertDialog Structure AlertDialog Title Content Cancel Delete

SimpleDialog: Multiple Choice Selection

When you need to present multiple options to the user, SimpleDialog is your friend. It's perfect for selection menus or choosing from a list of options.


void showSelectionDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return SimpleDialog(
        title: const Text('Choose an option'),
        children: [
          SimpleDialogOption(
            onPressed: () {
              Navigator.of(context).pop('option1');
            },
            child: const Text('Option 1'),
          ),
          SimpleDialogOption(
            onPressed: () {
              Navigator.of(context).pop('option2');
            },
            child: const Text('Option 2'),
          ),
          SimpleDialogOption(
            onPressed: () {
              Navigator.of(context).pop('option3');
            },
            child: const Text('Option 3'),
          ),
        ],
      );
    },
  ).then((value) {
    if (value != null) {
      print('Selected: $value');
    }
  });
}

Notice how we use .then() to handle the value returned when the dialog is closed. The Navigator.pop() call can include a return value that gets passed to the .then() callback.

Custom Dialogs: Building Your Own

Sometimes the built-in dialogs don't quite fit your needs. You can create custom dialogs by using the Dialog widget directly and designing your own layout.


void showCustomDialog(BuildContext context) {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (BuildContext context) {
      return Dialog(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(16),
        ),
        child: Padding(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(
                Icons.check_circle,
                color: Colors.green,
                size: 64,
              ),
              const SizedBox(height: 16),
              const Text(
                'Success!',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 8),
              const Text('Your action was completed successfully.'),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: const Text('OK'),
              ),
            ],
          ),
        ),
      );
    },
  );
}

The barrierDismissible parameter controls whether tapping outside the dialog closes it. Setting it to false forces users to interact with the dialog buttons.

Understanding Bottom Sheets

Bottom sheets slide up from the bottom of the screen, making them perfect for displaying additional content or actions without completely blocking the main content. They're less intrusive than dialogs and feel more natural on mobile devices.

BottomSheet: The Basic Implementation

The standard BottomSheet is a persistent overlay that slides up from the bottom. It's great for showing additional options or information.


void showBasicBottomSheet(BuildContext context) {
  showBottomSheet(
    context: context,
    builder: (BuildContext context) {
      return Container(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'Bottom Sheet Title',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            const Text('This is a basic bottom sheet example.'),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('Close'),
            ),
          ],
        ),
      );
    },
  );
}
Bottom Sheet Structure Main Content Bottom Sheet Content Area

ModalBottomSheet: The Dismissible Version

showModalBottomSheet creates a modal bottom sheet that blocks interaction with the content behind it until dismissed. This is the most commonly used bottom sheet variant.


void showModalBottomSheetExample(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(
        top: Radius.circular(20),
      ),
    ),
    builder: (BuildContext context) {
      return Container(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.only(bottom: 16),
              decoration: BoxDecoration(
                color: Colors.grey[300],
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.share),
              title: const Text('Share'),
              onTap: () {
                Navigator.pop(context);
                // Handle share action
              },
            ),
            ListTile(
              leading: const Icon(Icons.copy),
              title: const Text('Copy'),
              onTap: () {
                Navigator.pop(context);
                // Handle copy action
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete),
              title: const Text('Delete'),
              onTap: () {
                Navigator.pop(context);
                // Handle delete action
              },
            ),
          ],
        ),
      );
    },
  );
}

The drag handle (the small gray bar at the top) is a common UI pattern that indicates the sheet can be dragged down to dismiss. The shape parameter allows you to customize the rounded corners.

Expanding Bottom Sheets

Sometimes you need a bottom sheet that can expand to take up more screen space. You can achieve this by using isScrollControlled: true and controlling the height with FractionallySizedBox or DraggableScrollableSheet.


void showExpandingBottomSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    builder: (BuildContext context) {
      return DraggableScrollableSheet(
        initialChildSize: 0.5,
        minChildSize: 0.3,
        maxChildSize: 0.9,
        builder: (context, scrollController) {
          return Container(
            decoration: const BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.vertical(
                top: Radius.circular(20),
              ),
            ),
            child: Column(
              children: [
                Container(
                  width: 40,
                  height: 4,
                  margin: const EdgeInsets.symmetric(vertical: 12),
                  decoration: BoxDecoration(
                    color: Colors.grey[300],
                    borderRadius: BorderRadius.circular(2),
                  ),
                ),
                Expanded(
                  child: ListView.builder(
                    controller: scrollController,
                    itemCount: 20,
                    itemBuilder: (context, index) {
                      return ListTile(
                        title: Text('Item ${index + 1}'),
                        subtitle: Text('Description for item ${index + 1}'),
                      );
                    },
                  ),
                ),
              ],
            ),
          );
        },
      );
    },
  );
}

The DraggableScrollableSheet allows users to drag the sheet up and down, with initialChildSize setting the starting height (50% of screen), minChildSize setting the minimum height (30%), and maxChildSize setting the maximum height (90%).

Best Practices and Common Patterns

Returning Values from Dialogs

Both dialogs and bottom sheets can return values when they're dismissed. This is useful for getting user input or selections.


Future showDialogWithReturn(BuildContext context) async {
  final result = await showDialog(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: const Text('Choose an option'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              title: const Text('Option A'),
              onTap: () => Navigator.pop(context, 'A'),
            ),
            ListTile(
              title: const Text('Option B'),
              onTap: () => Navigator.pop(context, 'B'),
            ),
          ],
        ),
      );
    },
  );
  
  if (result != null) {
    print('User selected: $result');
  }
}

Handling Dialog State

When you need to update the dialog's content based on user interactions or async operations, you can use a StatefulBuilder inside your dialog.


void showLoadingDialog(BuildContext context) {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (BuildContext context) {
      return StatefulBuilder(
        builder: (context, setState) {
          return AlertDialog(
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const CircularProgressIndicator(),
                const SizedBox(height: 16),
                const Text('Processing...'),
              ],
            ),
          );
        },
      );
    },
  );
  
  // Simulate async operation
  Future.delayed(const Duration(seconds: 2), () {
    Navigator.of(context).pop();
    showDialog(
      context: context,
      builder: (context) => const AlertDialog(
        title: Text('Complete'),
        content: Text('Operation finished successfully!'),
      ),
    );
  });
}

Accessibility Considerations

Always ensure your dialogs and bottom sheets are accessible. Use semantic labels, provide clear button text, and ensure keyboard navigation works properly.


AlertDialog(
  title: const Text('Accessible Dialog'),
  content: const Text('This dialog follows accessibility best practices.'),
  actions: [
    TextButton(
      onPressed: () => Navigator.pop(context),
      child: const Text('Cancel'),
    ),
    TextButton(
      onPressed: () {
        // Action
        Navigator.pop(context);
      },
      child: const Text('Confirm'),
    ),
  ],
)

When to Use Dialogs vs Bottom Sheets

Choosing between dialogs and bottom sheets depends on your use case:

  • Use dialogs for: Critical confirmations, error messages, important alerts that require immediate attention, and when you need to block all user interaction.
  • Use bottom sheets for: Additional options, non-critical information, action menus, and when you want a less intrusive experience that doesn't completely block the underlying content.
Dialog vs Bottom Sheet Comparison Dialog Centered Blocks content Critical actions Confirmations Bottom Sheet Bottom aligned Less intrusive Action menus Additional options

Conclusion

Dialogs and bottom sheets are powerful tools for creating interactive, user-friendly Flutter applications. Dialogs excel at grabbing attention for critical actions, while bottom sheets provide a more subtle way to present additional options. By understanding when and how to use each, you can create interfaces that feel natural and intuitive to your users.

Remember to always consider accessibility, provide clear actions, and test your dialogs and bottom sheets on different screen sizes. With these components in your toolkit, you're well-equipped to build polished, professional Flutter applications.