adaptive_sheet
A zero-boilerplate, responsive Cupertino bottom sheet kit for Flutter.
Every Flutter developer has re-implemented the same modal bottom sheet: a
grabber, a title with a close button, drag-to-dismiss physics, keyboard
avoidance, and that clean Apple look. adaptive_sheet wraps all of it in a
single, readable call that returns a typed result — no boilerplate, no layout
overflow, no tablet jank.
final note = await AdaptiveSheet.show<String>(
context: context,
header: const AdaptiveSheetHeaderData(
title: 'New Note',
leading: Icon(CupertinoIcons.plus_rectangle_on_rectangle),
),
builder: (context, controller) => NoteComposer(
onSave: (text) => controller.pop(text),
),
);
Demo
The example app running on iOS and Android:
| iOS | Android |
|---|---|
![]() |
![]() |
Tap any tile in the example to try the five sheets: media picker, password reset, Google Maps–style location card, live theme customizer, and a gated terms reader.
Highlights
- Zero boilerplate.
AdaptiveSheet.show<T>(...)andcontext.showAdaptiveSheet<T>(...)return aFuture<T?>that resolves with whatever your content hands tocontroller.pop(value). - Native drag physics. Sheets resize and dismiss with the familiar overscroll-drag interaction (pull up to expand, pull down to collapse, fling to dismiss), snapping between min / default / max stops.
- Built-in header & closing controls. Grabber handle, leading icon,
bold title, subtitle, and a rounded
✕close button — all optional and fully customizable. - Keyboard aware. Sheets automatically rise above the software keyboard,
so inputs are never hidden. No
Padding(viewInsets)boilerplate. - Responsive by default. Full-width sheets on phones; width-constrained, iPad-style sheets (or an opt-in centered presentation) on tablets and wide displays.
- Frosted barrier. Optional Gaussian blur (
barrierBlur) for the genuine iOS scrim effect, or a classic dimmed barrier. - Data-loss safe.
onWillPopguards intercept every dismissal path (close button, barrier tap, drag, back button) so a dirty form is protected. - Safe areas, for real. Notches, status bars, and home indicators are respected on both platforms.
- Zero third-party runtime dependencies — only
flutterandcupertino_icons(for the✕glyph). - Fully documented and tested.
///docs on every public member and a broad widget-test suite covering dismissal, guards, and responsive widths.
Installation
Add the dependency to your pubspec.yaml:
dependencies:
adaptive_sheet: ^1.0.0
Then run:
flutter pub get
Quick start
import 'package:adaptive_sheet/adaptive_sheet.dart';
Future<void> pickFlavor(BuildContext context) async {
final flavor = await context.showAdaptiveSheet<String>(
header: const AdaptiveSheetHeaderData(
title: 'Pick a flavor',
leading: Icon(CupertinoIcons.fork_knife),
),
builder: (context, controller) => Column(
children: [
for (final name in ['Vanilla', 'Chocolate', 'Strawberry'])
ListTile(
title: Text(name),
onTap: () => controller.pop(name),
),
],
),
);
if (flavor != null) {
// Do something with the chosen flavor.
}
}
The sheet opens anchored to the bottom, drags like an iOS page sheet, and the
controller.pop(name) closes it, returning name to the awaiting call.
Usage
The header
`AdaptiveSheetHeaderData` is a const-friendly description of everything on
top of the sheet:
header: const AdaptiveSheetHeaderData(
title: 'Share this place',
subtitle: '3 attachments ready',
leading: Icon(CupertinoIcons.share),
onLeadingTap: _shareFast,
leadingTooltip: 'Share options', // screen-reader label
closeButtonTooltip: 'Dismiss',
// trailing: CustomWidget(), — replaces the close button entirely
),
The two controls intentionally read differently:
- Leading (left) is a quiet, transparent → ghost control (
leadingButtonColor/leadingButtonBackgroundColorto restyle) — a back/info affordance that never dismisses the sheet. - Close (right) is the prominent action: a filled, high-contrast
✕(closeButtonColor/closeButtonBackgroundColor/closeButtonSize), with a padded 44×44 keyboard-focusable tap target and a screen-reader label.
The close button renders only when the sheet is dismissible and
showCloseButton is true.
Returning a result
builder: (context, controller) => Column(
children: [
ElevatedButton(
onPressed: () => controller.pop('yes'),
child: const Text('Confirm'),
),
TextButton(
onPressed: controller.close, // dismiss with null
child: const Text('Cancel'),
),
],
),
pop(value) dismisses the sheet and resolves the awaited future with
value. close() dismisses with whatever result is currently recorded
(usually null).
Sticky footers
Pass the footer builder to pin action buttons below the scrollable content
— they stay visible no matter how far the user scrolls:
AdaptiveSheet.show<bool>(
context: context,
header: const AdaptiveSheetHeaderData(title: 'Terms of Service'),
footer: (context, controller) => FilledButton(
onPressed: () => controller.pop(true),
child: const Text('I Agree'),
),
builder: (context, controller) => _LongTermsBody(),
);
Action lists
For iOS-style action sheets (camera / gallery / destructive), compose
`AdaptiveSheetActionGroup` and `AdaptiveSheetAction`:
builder: (context, controller) => AdaptiveSheetActionGroup(
children: [
AdaptiveSheetAction(
icon: CupertinoIcons.camera,
label: 'Take Photo',
onTap: () => controller.pop(Source.camera),
),
AdaptiveSheetAction(
icon: CupertinoIcons.trash,
label: 'Remove Photo',
destructive: true,
onTap: () => controller.pop(Source.remove),
),
],
),
Guarding against data loss
Pass onWillPop (synchronous or asynchronous) to veto accidental
dismissals while a form is dirty — every path except a deliberate
controller.pop() from a primary action:
✕close button- barrier tap
- drag-to-dismiss fling
- system back button / gesture
onWillPop: () async {
if (!_isDirty) return true;
final discard = await showCupertinoDialog<bool>(
context: sheetContext, // captured inside `builder`
builder: (_) => CupertinoAlertDialog(
title: const Text('Discard changes?'),
actions: [
CupertinoDialogAction(
child: const Text('Keep editing'),
onPressed: () => Navigator.pop(_, false),
),
CupertinoDialogAction(
child: const Text('Discard'),
onPressed: () => Navigator.pop(_, true),
),
],
),
);
return discard ?? false;
},
Because controller.pop(value) is an explicit act, it never consults the guard —
a "Share", "Agree", or "Send" button that already validated is never vetoed.
Configuration
Every behavior is tuned through `AdaptiveSheetConfig` — reuse one
const AdaptiveSheetConfig() across sheets and branch with copyWith:
config: const AdaptiveSheetConfig(
presentation: AdaptiveSheetPresentation.sheet, // or .centered
initialHeightFraction: 0.62, // default opening height
minHeightFraction: 0.2, // smallest draggable height
maxHeightFraction: 0.95, // largest draggable height
enableDrag: true,
barrierDismissible: true,
barrierOpacity: 0.42,
barrierBlur: 8.0, // frosted-glass scrim
showGrabber: true,
tabletBreakpoint: 600, // width threshold for "large" screens
maxSheetWidth: 640, // max width on tablets / desktop
backgroundColor: CupertinoColors.systemBackground,
avoidKeyboard: true,
snapToStops: true,
),
expandContentToFit: true makes short confirmation/info sheets size
themselves to their content instead of opening to a fixed height.
Responsive behavior
| Viewport | sheet presentation |
centered presentation |
|---|---|---|
Phone (< 600dp) |
Full-width bottom sheet | Full-width bottom sheet |
| Tablet / desktop | Width-capped (640) bottom sheet | Centered, width-capped card |
The width-constraining kicks in automatically off the `MediaQuery` size, so
rotating a device or resplitting a window just works.
Examples
The example app in the repository is a runnable reference covering five real-world use cases, all backed by widget tests. Run it locally:
cd example
flutter run
- Media & image picker — an iOS-style action sheet (Camera / Library /
destructive Remove) built with
AdaptiveSheetActionGroup. - Password reset — a validating form with live strength-checklist chips, inline error feedback, keyboard avoidance, and a discard guard.
- Location details — a Google Maps–style place card: left-aligned title / rating / type / temporarily closed status, small share + close buttons on the right, tappable Save toggle, photo strip, and reviews.
- Live theme customizer — Light / Dark / System switcher plus an accent-color palette, restyling the whole app behind the transparent sheet live.
- Terms & conditions reader — long legal text that scrolls while a pinned footer stays visible, with an "I have read" checkbox gating the Agree button.
API overview
AdaptiveSheet— staticshow<T>/dismisshelpers.AdaptiveSheetContextExtension—context.showAdaptiveSheet<T>(...).AdaptiveSheetConfig— sizing, physics, barrier, and style knobs.AdaptiveSheetController<T>—pop(value)/close()to dismiss.AdaptiveSheetHeaderData/AdaptiveSheetHeader— the built-in header.AdaptiveSheetAction/AdaptiveSheetActionGroup— iOS action-sheet rows.AdaptiveSheetGrabber— the drag handle.AdaptiveSheetLayout<T>— the embeddable layout widget.AdaptiveSheetRoute<T>— the route used under the hood.AdaptiveSheetPresentation—sheetvscenteredplacement.
Author and Contact
Developed and maintained by Ebrahim Joy.
- Email: eebrahimjoy@gmail.com
- Website: eebrahimjoy.com
- GitHub: @eebrahimjoy
Contributing
Please report bugs and request features through the issue tracker. Pull requests are welcome.
License
Released under the MIT License.
Libraries
- adaptive_sheet
- A zero-boilerplate, responsive Cupertino bottom sheet kit for Flutter.

