adaptive_sheet 1.0.1
adaptive_sheet: ^1.0.1 copied to clipboard
A zero-boilerplate, responsive Cupertino bottom sheet kit for Flutter. Clean header, drag physics, keyboard handling, and adaptive tablet presentation.
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.

