show<T> static method
- required BuildContext context,
- required WidgetBuilder builder,
- double halfSize = 0.45,
- double? fullSize,
- GlassSheetState initialState = GlassSheetState.half,
- double fillThreshold = 0.60,
- LiquidGlassSettings? settings,
- Color? expandedColor,
- Color? expandedDarkColor,
- ValueChanged<
GlassSheetState> ? onStateChanged, - GlassSheetMode mode = GlassSheetMode.dismissible,
- double peekSize = 90.0,
- GlassQuality? quality,
- bool platformViewBackdrop = false,
- Color barrierColor = GlassDefaults.barrierColor,
- bool isDismissible = true,
- RouteSettings? routeSettings,
- double interactionScale = 1.01,
- bool enableInteractionGlow = true,
- bool enableSaturationGlow = true,
- LiquidGlassSettings? peekSettings,
- LiquidGlassSettings? halfSettings,
- LiquidGlassSettings? fullSettings,
- double stretch = 0.5,
- GlassModalSheetController? controller,
- GlassFillTransition fillTransition = GlassFillTransition.instant,
- bool showDragIndicator = true,
- Color? dragIndicatorColor,
- double dragIndicatorWidth = 36,
- double dragIndicatorHeight = 4,
- double dragIndicatorTopPadding = 8,
- double? topBorderRadius = 56,
- double? bottomBorderRadius,
- double? fullTopBorderRadius = 46,
- double? fullBottomBorderRadius,
- double horizontalMargin = 8.0,
- double bottomMargin = 8.0,
- double resistance = 0.08,
- double snapThreshold = 0.4,
- double velocityThreshold = 700.0,
- Color? glowColor,
- double glowRadius = 1.5,
- bool suppressInteractionOnChildren = false,
- EdgeInsetsGeometry? padding,
- bool enableTopFade = false,
- double topFadeHeight = 40.0,
- bool maintainContentGlass = true,
- LiquidGlassSettings? fullStateContentSettings,
- Set<
GlassSheetDetent> detents = const {GlassSheetDetent.medium, GlassSheetDetent.large}, - bool dismissible = true,
- double? peekHorizontalMargin,
- double? peekBottomMargin,
- double? peekWidth,
- double? peekTopBorderRadius,
- double? peekBottomRadius,
- GlassSheetPlacement placement = GlassSheetPlacement.automatic,
- GlassMorphAnchor? morphFrom,
- Rect? morphFromRect,
- MorphSpeed morphSpeed = MorphSpeed.normal,
- AnimationStyle? sheetAnimationStyle,
Shows a high-fidelity glass modal sheet.
The optional routeSettings are passed to the underlying dialog route,
allowing NavigatorObservers to identify the sheet by name and arguments.
They are also available through ModalRoute.of in builder.
Morphing from a trigger
Wrap the trigger in a GlassMorphTrigger and hand its GlassMorphAnchor
to morphFrom to present the sheet with the iOS 26 liquid morph instead
of the default slide-up: the trigger empties, a glass droplet detaches and
inflates as it travels, and lands as the sheet. Dismissing reverses it
back into the trigger. This is the same GlassMorphController engine
GlassMenu uses — see docs/LIQUID_MORPH_ENGINE.md.
GlassMorphTrigger(
builder: (context, anchor) => GlassButton(
onTap: () => GlassModalSheet.show(
context: context,
morphFrom: anchor,
builder: (context) => const MySheetBody(),
),
child: const Icon(CupertinoIcons.add),
),
)
The wrapper is what makes the trigger empty: nothing in Flutter lets one widget hide another it doesn't own, and a trigger still painting under the droplet reads as a duplicated button rather than a morph.
morphFromRect is the escape hatch for a trigger that can't be wrapped —
an explicit global Rect. The morph still runs, but since the trigger
stays painted the anchor blob is suppressed and the droplet blooms from
the rect's centre instead of stretching a teardrop out of it. Pass at most
one of morphFrom / morphFromRect.
Leave both null and the sheet presents exactly as it always has — the
slide transition is untouched. morphSpeed tunes the spring profile; the
default MorphSpeed.normal is the 375 ms native-parity profile.
sheetAnimationStyle sets the slide's durations and curves; the slide
otherwise takes 500 ms each way on Curves.easeOutQuart. A null
reverseDuration takes duration, and a null reverseCurve takes
curve. The morph runs on its own spring and ignores it.
The morph needs the metaball blend that draws the teardrop neck, so it
falls back to the slide transition when that is unavailable: on Skia/web
(ImageFilter.isShaderFilterSupported == false), in
GlassQuality.minimal, and under platformViewBackdrop. Reduce Motion is
honoured by the engine, which swaps in its instant spring so the sheet
resolves straight away instead of travelling.
Every dismissal morphs back into the trigger. One that leaves the sheet at rest — barrier tap, back gesture, GlassModalSheetController close — morphs from its resting frame. A swipe morphs from wherever the finger let go: below the lowest detent the sheet detaches from the bottom edge, follows the finger on both axes and shrinks uniformly with the vertical travel, and the release hands that exact frame to the morph. A swipe released short of the dismiss threshold springs back to the detent, at full size and centred.
This interactive shrink belongs to the morph, not to the sheet — the same
split iOS draws between its zoom transition and its detents. Present
without morphFrom and the sheet keeps its plain slide-away dismissal.
Implementation
static Future<T?> show<T>({
required BuildContext context,
required WidgetBuilder builder,
double halfSize = 0.45,
double? fullSize,
GlassSheetState initialState = GlassSheetState.half,
double fillThreshold = 0.60,
LiquidGlassSettings? settings,
Color? expandedColor,
Color? expandedDarkColor,
ValueChanged<GlassSheetState>? onStateChanged,
GlassSheetMode mode = GlassSheetMode.dismissible,
double peekSize = 90.0,
GlassQuality? quality,
bool platformViewBackdrop = false,
Color barrierColor = GlassDefaults.barrierColor,
bool isDismissible = true,
bool useRootNavigator = false,
RouteSettings? routeSettings,
double interactionScale = 1.01,
bool enableInteractionGlow = true,
bool enableSaturationGlow = true,
LiquidGlassSettings? peekSettings,
LiquidGlassSettings? halfSettings,
LiquidGlassSettings? fullSettings,
double stretch = 0.5,
GlassModalSheetController? controller,
GlassFillTransition fillTransition = GlassFillTransition.instant,
bool showDragIndicator = true,
Color? dragIndicatorColor,
double dragIndicatorWidth = 36,
double dragIndicatorHeight = 4,
double dragIndicatorTopPadding = 8,
double? topBorderRadius = 56,
double? bottomBorderRadius,
double? fullTopBorderRadius = 46,
double? fullBottomBorderRadius,
double horizontalMargin = 8.0,
double bottomMargin = 8.0,
double resistance = 0.08,
double snapThreshold = 0.4,
double velocityThreshold = 700.0,
Color? glowColor,
double glowRadius = 1.5,
bool suppressInteractionOnChildren = false,
EdgeInsetsGeometry? padding,
bool enableTopFade = false,
double topFadeHeight = 40.0,
bool maintainContentGlass = true,
LiquidGlassSettings? fullStateContentSettings,
Set<GlassSheetDetent> detents = const {
GlassSheetDetent.medium,
GlassSheetDetent.large
},
bool dismissible = true,
double? peekHorizontalMargin,
double? peekBottomMargin,
double? peekWidth,
double? peekTopBorderRadius,
double? peekBottomRadius,
GlassSheetPlacement placement = GlassSheetPlacement.automatic,
GlassMorphAnchor? morphFrom,
Rect? morphFromRect,
MorphSpeed morphSpeed = MorphSpeed.normal,
AnimationStyle? sheetAnimationStyle,
}) {
assert(
morphFrom == null || morphFromRect == null,
'Pass either morphFrom (the GlassMorphAnchor from a GlassMorphTrigger) '
'or morphFromRect (an explicit global rect) — not both.');
assert(
detents.isNotEmpty,
'GlassModalSheet.show() needs at least one detent — add medium '
'and/or large (small alone is a floor, not a resting height).');
assert(() {
if (mode == GlassSheetMode.persistent &&
barrierColor == const Color(0x00000000)) {
debugPrint(
'[GlassModalSheet] WARNING: show() with persistent mode and '
'transparent barrier does NOT provide true hit-through interaction. '
'Use GlassModalSheetScaffold directly for maps-style hit-through UI.',
);
}
return true;
}());
// Fall back to an offered detent if the caller asked to open on one
// that isn't in the set (the assert guarantees the set is non-empty).
GlassSheetState resolvedInitialState = initialState;
if (initialState == GlassSheetState.half &&
!detents.contains(GlassSheetDetent.medium)) {
resolvedInitialState = GlassSheetState.full;
} else if (initialState == GlassSheetState.full &&
!detents.contains(GlassSheetDetent.large)) {
resolvedInitialState = GlassSheetState.half;
}
final effectiveController = controller ?? GlassModalSheetController();
bool isClosing = false;
// Resolved once, before the route is pushed: the trigger's rect has to be
// read while the trigger is still laid out, and the render capabilities
// decide which transition the route is built with in the first place.
final morphTriggerRect = _resolveMorphTriggerRect(morphFrom, morphFromRect);
final morphing = morphTriggerRect != null &&
_supportsMorph(
context,
quality: quality,
platformViewBackdrop: platformViewBackdrop,
);
// The morph owns its own spring clock; the route duration only has to be
// long enough to keep the page mounted (and the barrier fading) for as long
// as the morph runs — most visibly on the way out, where the route reverse
// is the window the closing morph plays in.
const slideDuration = Duration(milliseconds: 500);
final transitionDuration = morphing
? _morphRouteDuration(morphSpeed)
: sheetAnimationStyle?.duration ?? slideDuration;
final reverseTransitionDuration = morphing
? transitionDuration
: sheetAnimationStyle?.reverseDuration ??
sheetAnimationStyle?.duration ??
slideDuration;
// On a half-folded display the route presents into one side of the fold,
// as [DisplayFeatureSubScreen] does for any dialog. The sheet is laid out
// in that side rather than across the screen.
final subScreen = _subScreenFor(context);
return Navigator.of(context, rootNavigator: useRootNavigator)
.push<T>(_GlassModalSheetRoute<T>(
anchorPoint: subScreen?.center,
barrierDismissible: isDismissible,
barrierLabel: 'Dismiss',
barrierColor: morphing
// When morphing, the presenter drives the scrim itself via the liquid
// spring so it stays in lockstep with the blob. A route-level barrier
// would fade linearly on an independent clock, creating the "two
// separate things" visual that GlassMenu avoids by having no route
// barrier at all. Pass transparent here so the route's built-in
// linear fade does not run alongside the spring-driven scrim.
? const Color(0x00000000)
: barrierColor,
settings: routeSettings,
transitionDuration: transitionDuration,
reverseTransitionDuration: reverseTransitionDuration,
transitionBuilder: (context, animation, secondaryAnimation, child) {
// Morphing: the droplet IS the transition. Mapping the engine onto this
// linear route animation would flatten the J-curve and the underdamped
// catch, so the page passes straight through.
if (morphing) return child;
return SlideTransition(
position: Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero)
.animate(
CurvedAnimation(
parent: animation,
curve: sheetAnimationStyle?.curve ?? Curves.easeOutQuart,
reverseCurve: sheetAnimationStyle?.reverseCurve,
),
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) =>
_inSubScreen(context, subScreen, () {
final scaffold = GlassModalSheetScaffold(
controller: effectiveController,
halfSize: halfSize,
fullSize: fullSize,
initialState: resolvedInitialState,
fillThreshold: fillThreshold,
settings: settings,
expandedColor: expandedColor,
expandedDarkColor: expandedDarkColor,
mode: mode,
peekSize: peekSize,
quality: quality,
platformViewBackdrop: platformViewBackdrop,
interactionScale: interactionScale,
enableInteractionGlow: enableInteractionGlow,
enableSaturationGlow: enableSaturationGlow,
peekSettings: peekSettings,
halfSettings: halfSettings,
fullSettings: fullSettings,
stretch: stretch,
fillTransition: fillTransition,
showDragIndicator: showDragIndicator,
dragIndicatorColor: dragIndicatorColor,
dragIndicatorWidth: dragIndicatorWidth,
dragIndicatorHeight: dragIndicatorHeight,
dragIndicatorTopPadding: dragIndicatorTopPadding,
topBorderRadius: topBorderRadius,
bottomBorderRadius: bottomBorderRadius,
fullTopBorderRadius: fullTopBorderRadius,
fullBottomBorderRadius: fullBottomBorderRadius,
horizontalMargin: horizontalMargin,
bottomMargin: bottomMargin,
resistance: resistance,
snapThreshold: snapThreshold,
velocityThreshold: velocityThreshold,
glowColor: glowColor,
glowRadius: glowRadius,
suppressInteractionOnChildren: suppressInteractionOnChildren,
padding: padding,
enableTopFade: enableTopFade,
topFadeHeight: topFadeHeight,
maintainContentGlass: maintainContentGlass,
fullStateContentSettings: fullStateContentSettings,
detents: detents,
dismissible: dismissible,
barrierDismissible: isDismissible,
peekHorizontalMargin: peekHorizontalMargin,
peekBottomMargin: peekBottomMargin,
peekWidth: peekWidth,
peekTopBorderRadius: peekTopBorderRadius,
peekBottomRadius: peekBottomRadius,
placement: placement,
onStateChanged: (state) {
onStateChanged?.call(state);
if (state == GlassSheetState.hidden && !isClosing) {
isClosing = true;
Navigator.of(context).pop();
}
},
body: const SizedBox.shrink(),
sheet: builder(context),
);
if (!morphing) return scaffold;
return GlassSheetMorphPresenter(
routeAnimation: animation,
triggerRect: morphTriggerRect,
anchor: morphFrom,
speed: morphSpeed,
restingState: resolvedInitialState,
controller: effectiveController,
// Built from the same inputs as the sheet's own _buildGeometry, so
// the droplet aims at the detent the sheet will actually rest at.
geometry: SheetGeometry(
mode: mode,
halfSize: halfSize,
fullSize: fullSize ?? _stripFullSize(context),
peekSize: peekSize,
enablePeek: SheetGeometry.resolvePeek(
detents: detents,
mode: mode,
),
enableHalf: detents.contains(GlassSheetDetent.medium),
enableFull: detents.contains(GlassSheetDetent.large),
dismissible: dismissible,
),
horizontalMargin: horizontalMargin,
bottomMargin: bottomMargin,
topBorderRadius: topBorderRadius,
fullTopBorderRadius: fullTopBorderRadius,
bottomBorderRadius: bottomBorderRadius,
fullBottomBorderRadius: fullBottomBorderRadius,
settings: settings,
peekSettings: peekSettings,
halfSettings: halfSettings,
fullSettings: fullSettings,
expandedColor: expandedColor,
expandedDarkColor: expandedDarkColor,
quality: quality,
peekHorizontalMargin: peekHorizontalMargin,
peekBottomMargin: peekBottomMargin,
peekWidth: peekWidth,
peekTopBorderRadius: peekTopBorderRadius,
platformViewBackdrop: platformViewBackdrop,
// The route-level barrier is transparent above; the presenter drives
// the scrim itself via the liquid spring for single-unified-motion.
barrierColor: barrierColor,
placement: placement,
child: scaffold,
);
}),
));
}