M3E Bottom Sheet

Intro

A Flutter package providing an expressive, Material 3 Expressive modal bottom sheet with spring physics entrance motion, grounded bottom anchoring, drag handle, header layout, theming, and extensive customization options.

It is a drop-in replacement for showModalBottomSheet with full parameter compatibility. Any widget returned by the builder is automatically wrapped in an M3EBottomSheet container, and theming works both app-wide (via M3EBottomSheetThemeData) and per-subtree (via the M3EBottomSheetTheme inherited widget).

Part of the M3E (Material 3 Expressive) component family (see also m3e_core).


🎮 Interactive Demo

You can try out the package demo here: m3e_core demo


🚀 Features

  • Spring Physics Motion — expressive entrance motion powered by motor via M3EMotion presets instead of a linear curve
  • Grounded Bottom Anchoring — an overshoot "skirt" eliminates any gap or detachment between the sheet and the screen bottom during spring overshoot
  • Drop-in Replacement — full parameter compatibility with showModalBottomSheet via showM3EModalBottomSheet
  • Automatic Wrapping — pass any widget; it is wrapped in an M3EBottomSheet container for you
  • Drag Handle — default rounded pill indicator, a fully custom widget, or none at all
  • Header Layout — optional title + trailing actions row
  • Rich Styling — background color, surface tint, shadow color, elevation, custom shape, corner radius, padding, clip behavior, drag handle color/size/padding
  • ThemingM3EBottomSheetThemeData theme extension plus an M3EBottomSheetTheme inherited widget for subtree-scoped defaults
  • Motion Presets — all M3EMotion expressive/standard spatial & effects presets, or fully custom springs via M3EMotion.custom
  • Result Returning — awaits the value passed to Navigator.pop, just like the standard sheet
  • Inline UsageM3EBottomSheet can also be embedded directly in a widget tree

📦 Installation

Important

Flutter 3.47+ & material_ui Requirement: m3e_bottom_sheet uses the standalone material_ui package decoupled in Flutter 3.47.0.

  • Requires Flutter SDK >=3.47.0.
  • Ensure your app imports package:material_ui/material_ui.dart (or run dart fix --apply --code=migrate_design_widgets).

Add m3e_bottom_sheet and material_ui to your pubspec.yaml:

dependencies:
  material_ui: ^1.1.0
  m3e_bottom_sheet: ^0.0.1
import 'package:material_ui/material_ui.dart';
import 'package:m3e_bottom_sheet/m3e_bottom_sheet.dart';

🧩 Quick Start

Basic Modal Sheet

showM3EModalBottomSheet(
  context: context,
  builder: (context) => M3EBottomSheet(
    title: const Text('Theme Settings'),
    actions: [
      IconButton(
        icon: const Icon(Icons.close_rounded),
        onPressed: () => Navigator.pop(context),
      ),
    ],
    child: const Text('Sheet content'),
  ),
);

Auto-wrapping Plain Widgets

Any widget returned by the builder is automatically wrapped in an M3EBottomSheet container (spring motion + 28dp top radius). Pass showDragHandle: true to add the handle:

showM3EModalBottomSheet(
  context: context,
  showDragHandle: true,
  builder: (context) => const Text('Plain content'),
);

Custom Drag Handle

showM3EModalBottomSheet(
  context: context,
  builder: (context) => M3EBottomSheet(
    title: const Text('Custom handle'),
    dragHandle: Container(
      margin: const EdgeInsets.only(top: 16, bottom: 12),
      width: 56,
      height: 6,
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.primary,
        borderRadius: BorderRadius.circular(3),
      ),
    ),
    child: const Text('Sheet content'),
  ),
);

Set showDragHandle: false to remove the handle entirely.

Scrollable Content

Long content should live in a bounded scrollable so the sheet stays draggable and performs well:

showM3EModalBottomSheet(
  context: context,
  builder: (context) => M3EBottomSheet(
    title: const Text('Contacts'),
    child: SizedBox(
      height: 320,
      child: ListView.builder(
        itemCount: 30,
        itemBuilder: (context, index) => ListTile(
          leading: CircleAvatar(child: Text('${index + 1}')),
          title: Text('Contact ${index + 1}'),
        ),
      ),
    ),
  ),
);

Custom Style & Motion

showM3EModalBottomSheet(
  context: context,
  style: M3EBottomSheetStyle(
    borderRadius: 36,
    elevation: 4,
    motion: M3EMotion.expressiveSpatialSlow,
    dragHandleColor: Colors.deepPurple,
  ),
  builder: (context) => const Text('Styled sheet'),
);

Theming

Register the theme extension on your ThemeData to provide app-wide defaults:

MaterialApp(
  theme: ThemeData(
    extensions: [
      M3EBottomSheetThemeData(
        style: M3EBottomSheetStyle(dragHandleColor: Colors.deepPurple),
      ),
    ],
  ),
);

Or scope defaults to a subtree with the inherited widget:

M3EBottomSheetTheme(
  data: const M3EBottomSheetThemeData(
    style: M3EBottomSheetStyle(motion: M3EMotion.expressiveSpatialSlow),
  ),
  child: MyPage(),
);

Returning a Result

showM3EModalBottomSheet returns a Future<T?> with the value passed to Navigator.pop — just like showModalBottomSheet:

final selected = await showM3EModalBottomSheet<String>(
  context: context,
  builder: (sheetContext) => M3EBottomSheet(
    title: const Text('Pick a color'),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(
          title: const Text('Red'),
          onTap: () => Navigator.of(sheetContext).pop('Red'),
        ),
        ListTile(
          title: const Text('Green'),
          onTap: () => Navigator.of(sheetContext).pop('Green'),
        ),
      ],
    ),
  ),
);

Inline (Non-modal) Usage

M3EBottomSheet can also be embedded directly in a widget tree:

M3EBottomSheet(
  showDragHandle: true,
  animateEntrance: false,
  child: const Text('Inline sheet'),
)

See the example app for a complete demo covering every feature.


📖 Detailed API Guide

1. M3EMotion

Spring physics configuration with 14 built-in presets and custom spring support. The bottom sheet uses expressiveSpatialDefault unless overridden.

🏗️ Spatial Presets (Sheet Entrance Motion)

Used to drive the sheet's spring entrance and exit.

Preset Stiffness Damping Description
standardSpatialFast 1400 0.9 Snappy spring for responsive feel
standardSpatialDefault 700 0.9 Balanced spring for general use
standardSpatialSlow 300 0.9 Relaxed spring for dramatic feel
expressiveSpatialFast 800 0.6 Bouncier spring for expressive feel
expressiveSpatialDefault 380 0.8 Bouncy, balanced spring (package default)
expressiveSpatialSlow 200 0.8 Very bouncy for dramatic feel

✨ Effects Presets (Opacity/Scale)

Used for content animations like cross-fades.

Preset Stiffness Damping Description
standardEffectsFast 3800 1.0 Snappy effect animation
standardEffectsDefault 1600 1.0 Balanced effect animation
standardEffectsSlow 800 1.0 Relaxed effect animation
expressiveEffectsFast 3800 1.0 Snappy expressive effect
expressiveEffectsDefault 1600 1.0 Balanced expressive effect
expressiveEffectsSlow 800 1.0 Relaxed expressive effect

🧰 Utility Presets

Spring animations for overflow menus and popups.

Preset Stiffness Damping Description
standardOverflow 1600 0.85 Spring for overflow menus
standardPopup 1000 0.6 Bouncy spring for popup menus

🛠️ Custom Motion

M3EMotion.custom(stiffness: 1200, damping: 0.5)

2. M3EBottomSheetStyle

Style configuration for M3EBottomSheet and showM3EModalBottomSheet.

Field Type Default Description
backgroundColor Color? cs.surfaceContainerHigh Background color of the sheet
surfaceTintColor Color? cs.surfaceTint Surface tint applied when elevated
shadowColor Color? cs.shadow @ 35% alpha Shadow color cast when elevated
elevation double? 0 Elevation of the sheet
shape ShapeBorder? Custom shape border (overrides borderRadius)
borderRadius double 28.0 Top corner radius per M3 Expressive guidelines
dragHandleColor Color? cs.onSurfaceVariant @ 40% alpha Color of the drag handle indicator
dragHandleSize Size Size(32, 4) Dimensions of the drag handle
dragHandlePadding EdgeInsetsGeometry vertical: 16 Padding around the drag handle
motion M3EMotion? expressiveSpatialDefault Spring preset driving the entrance motion
padding EdgeInsetsGeometry fromLTRB(24, 0, 24, 24) Content padding inside the sheet
clipBehavior Clip Clip.antiAlias Clip behavior for the sheet container

cs refers to the ambient ColorScheme.

Member Returns Description
copyWith(...) M3EBottomSheetStyle Copy of this style with the given fields replaced
lerp(a, b, t) M3EBottomSheetStyle? Linearly interpolates between two styles
const M3EBottomSheetStyle(
  backgroundColor: Color(0xFFFFF3E0),
  surfaceTintColor: Colors.deepOrange,
  elevation: 6,
  borderRadius: 48,
  dragHandleColor: Color(0xFF26A69A),
  dragHandleSize: Size(56, 6),
  dragHandlePadding: EdgeInsets.symmetric(vertical: 20),
  motion: M3EMotion.expressiveSpatialSlow,
  padding: EdgeInsets.fromLTRB(16, 0, 16, 32),
)

3. M3EBottomSheetThemeData & M3EBottomSheetTheme

Theming for app-wide or subtree-scoped sheet defaults.

M3EBottomSheetThemeData is a ThemeExtension that registers default styling on ThemeData:

Field Type Default Description
style M3EBottomSheetStyle? M3EBottomSheetStyle() Default style for M3EBottomSheet widgets
Member Returns Description
copyWith(style) M3EBottomSheetThemeData Copy with the given style replaced
lerp(other, t) M3EBottomSheetThemeData Interpolates between two theme datas

M3EBottomSheetTheme is an InheritedTheme that scopes defaults to a subtree:

Field Type Description
data M3EBottomSheetThemeData Properties applied to child M3EBottomSheet widgets
child Widget Subtree the defaults apply to
Method Returns Description
M3EBottomSheetTheme.of(context) M3EBottomSheetThemeData Closest inherited data, else the ThemeData extension, else defaults

4. M3EBottomSheet

The Material 3 Expressive sheet container widget. Provides rounded top corners (default 28dp), an optional top drag handle, header layout, content padding, and spring physics entrance motion powered by motor.

Parameter Type Default Description
child Widget The primary content of the sheet
style M3EBottomSheetStyle? Custom visual styling for this sheet
showDragHandle bool true Whether to display the top drag handle bar
dragHandle Widget? Custom drag handle widget replacing the default pill
title Widget? Title widget displayed below the drag handle
actions List<Widget>? Action widgets aligned to the trailing side of the title
padding EdgeInsetsGeometry? style-derived Content padding; overrides style.padding
backgroundColor Color? style-derived Background color; overrides style.backgroundColor
animateEntrance bool true Whether to play the spring entrance motion when mounted
M3EBottomSheet(
  title: const Text('Basic sheet'),
  actions: [
    IconButton(
      icon: const Icon(Icons.close_rounded),
      tooltip: 'Close',
      onPressed: () => Navigator.of(context).pop(),
    ),
  ],
  showDragHandle: true,
  animateEntrance: true,
  style: const M3EBottomSheetStyle(
    borderRadius: 36,
    elevation: 4,
    dragHandleColor: Colors.deepPurple,
  ),
  child: const Text('Sheet content'),
)

5. showM3EModalBottomSheet<T>

Drop-in replacement for Flutter's showModalBottomSheet with expressive spring physics entrance/exit driven by motor and grounded bottom anchoring. Returns a Future<T?> that completes with the value passed to Navigator.pop.

Parameter Type Default Description
context BuildContext Build context used to look up the navigator
builder WidgetBuilder Builder for the sheet content
backgroundColor Color? Background color override
barrierLabel String? Semantic label for the modal barrier
elevation double? Elevation override
shape ShapeBorder? Shape override
clipBehavior Clip? Clip behavior override
constraints BoxConstraints? Size constraints for the sheet
barrierColor Color? Color of the modal barrier
isScrollControlled bool true Whether the sheet can take the full screen height
scrollControlDisabledMaxHeightRatio double 9/16 Max height ratio when not scroll controlled
useRootNavigator bool false Whether to push the route on the root navigator
isDismissible bool true Whether tapping the barrier dismisses the sheet
enableDrag bool true Whether the sheet can be dragged to close
showDragHandle bool? false when auto-wrapping Adds the default handle when the builder returns a plain widget
useSafeArea bool true Whether to avoid system insets
routeSettings RouteSettings? Settings for the sheet route
transitionAnimationController AnimationController? Custom transition animation controller
anchorPoint Offset? Anchor point for positioning
sheetAnimationStyle AnimationStyle? Custom sheet animation style
style M3EBottomSheetStyle? Style applied to the sheet container
motion M3EMotion? Motion override merged into the effective style
final selected = await showM3EModalBottomSheet<String>(
  context: context,
  isScrollControlled: true,
  isDismissible: true,
  enableDrag: true,
  barrierColor: Colors.black.withValues(alpha: 0.4),
  motion: M3EMotion.expressiveSpatialFast,
  style: const M3EBottomSheetStyle(borderRadius: 36, elevation: 4),
  builder: (sheetContext) => M3EBottomSheet(
    title: const Text('Pick a color'),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(
          title: const Text('Red'),
          onTap: () => Navigator.of(sheetContext).pop('Red'),
        ),
      ],
    ),
  ),
);

Note

If the builder returns an M3EBottomSheet, the top-level style, backgroundColor, elevation, and motion overrides are merged into it. If it returns any other widget, the child is wrapped in an M3EBottomSheet container for you (showDragHandle defaults to false when auto-wrapping).


6. Style Resolution Order

M3EBottomSheet resolves its effective style with the following precedence (highest wins):

  1. Widget-level backgroundColor and padding parameters
  2. Widget-level style (M3EBottomSheetStyle)
  3. M3EBottomSheetTheme.of(context) — inherited widget or ThemeData extension
  4. Built-in defaults (M3EBottomSheetStyle())

🐞 Found a bug? or ✨ You have a Feature Request?

Feel free to open an Issue or Contribute to the project.

Hope You Love It!


Credits

  • Motor Pub Package for Expressive Animations
  • Claude and Gemini for helping me with the code and documentation.

Radhe Radhe 🙏