Skip to content
Blog

Bottom Sheets in Flutter: showModalBottomSheet Patterns Done Right

Modal and persistent bottom sheets done right: full-height layouts, drag handles, safe areas, keyboard-aware forms, and awaited results.

Published on • November 2, 2026

AI Assistant

Bottom sheets let users finish short tasks without leaving the screen, and Flutter ships modal and persistent variants out of the box (docs.flutter.dev). The API is small; height, keyboard, and dismissal details are what separate a clean sheet from a broken one.

showModalBottomSheet vs showBottomSheet

showModalBottomSheet pushes a route with a barrier — the app behind is dimmed and blocked until the sheet pops. showBottomSheet attaches a persistent sheet to the nearest Scaffold that the user can still interact around; use it only for non-blocking status like progress or sync state.

// Modal: blocks the UI and resolves with a value.
final action = await showModalBottomSheet<String>(
  context: context,
  builder: (ctx) => ListTile(
    title: const Text('Archive conversation'),
    onTap: () => Navigator.pop(ctx, 'archive'),
  ),
);

Full-height sheets: isScrollControlled + DraggableScrollableSheet

A modal sheet is capped at half the screen by default. Pass isScrollControlled: true to fill the display, hand the builder a DraggableScrollableSheet for drag-between-snap-points behavior, and remember that the builder’s scrollController is mandatory — without it the sheet renders but can never be dragged.

showModalBottomSheet<void>(
  context: context,
  isScrollControlled: true,
  useSafeArea: true,
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
  builder: (ctx) => DraggableScrollableSheet(
    expand: false, initialChildSize: 0.6, maxChildSize: 0.95,
    builder: (ctx, sc) => ListView.builder(
      controller: sc, itemCount: 40,
      itemBuilder: (_, i) => ListTile(title: Text('Item $i'))),
  ),
);

Keyboard-aware forms with PopScope

Pad forms with MediaQuery.viewInsets.bottom instead of a fixed value so the keyboard never covers the submit button, and wrap the sheet in PopScope when back-navigation must be intercepted explicitly.

final saved = await showModalBottomSheet<bool>(
  context: context,
  isScrollControlled: true,
  builder: (ctx) => PopScope(
    canPop: false,
    onPopInvokedWithResult: (d, _) => d ? null : Navigator.pop(ctx, false),
    child: Padding(
      padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom + 24),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        const TextField(autofocus: true, hintText: 'Title'),
        FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Save')),
      ]),
    ),
  ),
);

Gotchas and best practices

  • Add clipBehavior: Clip.antiAlias with shape, or children paint straight over the rounded corners.
  • Give the sheet root mainAxisSize: MainAxisSize.min, or short content stretches oddly to full height.
  • Use isDismissible: false + enableDrag: false for destructive confirmations.
  • The future resolves null on dismiss — null-check it and guard setState with if (!mounted) return;.
  • Mark the sheet title Semantics(header: true) so screen readers announce the new context.

A bottom sheet done right is a shaped, safe-area-aware modal route that resolves exactly one awaited value — build it once, reuse it everywhere.