M3E Bottom Sheet

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
motorviaM3EMotionpresets 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
showModalBottomSheetviashowM3EModalBottomSheet - Automatic Wrapping — pass any widget; it is wrapped in an
M3EBottomSheetcontainer 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
- Theming —
M3EBottomSheetThemeDatatheme extension plus anM3EBottomSheetThemeinherited widget for subtree-scoped defaults - Motion Presets — all
M3EMotionexpressive/standard spatial & effects presets, or fully custom springs viaM3EMotion.custom - Result Returning — awaits the value passed to
Navigator.pop, just like the standard sheet - Inline Usage —
M3EBottomSheetcan 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 rundart 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):
- Widget-level
backgroundColorandpaddingparameters - Widget-level
style(M3EBottomSheetStyle) M3EBottomSheetTheme.of(context)— inherited widget orThemeDataextension- 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 🙏
Libraries
- bottom_sheet/bottom_sheet
- bottom_sheet/core/m3e_bottom_sheet_route
- bottom_sheet/m3e_bottom_sheet
- bottom_sheet/style/m3e_bottom_sheet_style
- bottom_sheet/style/m3e_bottom_sheet_theme
- common/m3e_common
- m3e_bottom_sheet
- Material 3 Expressive (M3E) bottom sheet components for Flutter.