Morph Route โจ
iOS 26-inspired container morph for Flutter.
Tap a card, watch it expand into a full screen โ and the screen behind it blur, tint, and step back like it's making room. morph_route is a modified OpenContainer plus two helper widgets that wire all of this up for you.
What you get out of the box:
- ๐ซ Backdrop blur on the underlying screen as the morph opens.
- ๐จ Accent scrim โ a low-alpha brand color washed over the host UI for warmth.
- ๐ฒ iOS 26-style 3D tilt that peaks at the morph's midpoint and resolves flat on both ends.
- ๐ฆ Scaffold-level recede so the host UI scales down in lock-step with the morph.
๐ฆ Install
dependencies:
morph_route: ^0.1.2
Or flutter pub add morph_route.
๐ 30-second quickstart
import 'package:flutter/material.dart';
import 'package:morph_route/morph_route.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
// 1. Wrap your body so it scales down behind any active morph route.
body: MorphRecede(
child: Center(
// 2. Use MorphTile in place of a regular button/card.
child: MorphTile(
transitionDuration: const Duration(milliseconds: 460),
reverseTransitionDuration: const Duration(milliseconds: 340),
closedColor: Colors.white,
openColor: Colors.white,
closedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
closedShadows: const [
BoxShadow(blurRadius: 4, color: Colors.black12),
],
// The premium bits:
openBlurSigma: 12,
scrimColor: const Color(0x14D97757), // 8% coral
peakTiltY: 0.09, // ~5ยฐ Y rotation mid-flight
// Press feedback:
pressedScale: 0.98,
pressedOverlayColor: Colors.black12,
onTap: () { /* haptics, analytics */ },
closedBuilder: (context) => SizedBox(
width: 160, height: 80,
child: const Center(child: Text('Open')),
),
openBuilder: (context, _) => const DetailScreen(),
),
),
),
);
}
}
๐งฉ API surface
| Widget | What it does | When to reach for it |
|---|---|---|
MorphTile |
A pressable tile that morphs into a destination route. Wraps OpenContainer with idiomatic press feedback (subtle scale + tinted overlay). |
95% of callers โ start here. |
MorphRecede |
Wraps a scaffold body so it scales down (and optionally translates) as a morph route opens, synced to OpenContainer.activeProgress. |
The "host UI is stepping back" feel. |
OpenContainer |
The morph route itself. | When you need finer control than MorphTile gives โ programmatic open, custom press feedback, etc. |
Full dartdoc lives next to each class in lib/src/.
๐ Tunable knobs you'll actually reach for
On MorphTile / OpenContainer:
transitionDuration/reverseTransitionDurationโ open vs close timing. Defaults are300ms/ same. We recommend460msopen,340msclose (Emil Kowalski's "exits should be snappier than entrances").openBlurSigmaโ Gaussian blur on the underlying screen at full open.0= off.8โ12reads as "out of focus" without becoming frosted glass.scrimColorโ color washed over the underlying screen. Default transparent. Low-alpha brand accent (e.g.0x14D97757) gives a warm signal without being a tint.peakTiltY/peakTiltXโ radians of rotation at the morph's midpoint (0 at both endpoints, peak at 0.5, followssin(ฯยทt)).0.05โ0.10 radโ 3ยฐโ6ยฐ. More than that flips.tiltPerspectiveโ1 / focal-lengthfor the 3D matrix.0.0015โ 660-unit focal length.transitionTypeโfade(default) orfadeThrough.fadeThroughcross-fades throughmiddleColor.
On MorphRecede:
scaleFloorโ scale at full open (default0.94).1.0disables.curveโ applied to the raw progress before scaling. DefaultCurves.ease(gentle, symmetric on close). Aggressive ease-out feels "snappy" on close.progressโ override the source notifier. Defaults toOpenContainer.activeProgress.
โ ๏ธ Caveats
- One morph at a time.
OpenContainer.activeProgressis a single globalValueNotifier<double>, so two morphs running concurrently would clobber each other's published value. Fine for typical UX. If you need multi-morph, fork the package or pass an explicit notifier (not currently supported). BackdropFilteris real GPU work. The package skips theBackdropFilterlayer entirely whenblurSigma <= 0.01, but at non-zero values it samples and blurs the underlying tree every frame of the morph. Budget accordingly on low-end devices.- Reduced-motion not honored yet.
MediaQuery.disableAnimationsisn't respected. Planned follow-up. - Tilt and hit-testing. A tilted card has a tilted hit region. We don't accept input mid-morph anyway, so it's a non-issue in practice.
Attribution
OpenContainer is vendored from package:animations (BSD-3, Flutter Authors), with several modifications documented in the file header. We retain the original copyright and add ours, both under BSD-3.
License
BSD-3-Clause. See LICENSE.
Libraries
- morph_route
morph_routeโ iOS 26-inspired container morph for Flutter.