five_page_navigation 3.0.0
five_page_navigation: ^3.0.0 copied to clipboard
Snapchat-style navigation between a center page and four side pages with edge swipes, 14 built-in transitions, previews, haptics and a controller.
Five Page Navigation #
Snapchat-style navigation for Flutter: one center page and up to four side pages (left, right, top, bottom), opened with edge swipes and animated with any of 14 built-in transitions.
Features #
- Edge swipes from the center page open the page on that side; side pages can be swiped back.
- 14 transitions: slide, parallax, cover, depth, reveal, fade, fade through, shared axis, zoom, scale, cube, flip, rotate and none, plus fully custom ones.
- Gestures that play well with your content: swipes only start inside configurable edge zones and in the right direction, so lists, page views and buttons keep working. Flings are supported.
- Back button and
AppBarintegration: the Android back button,Navigator.popand theAppBarback arrow on a side page all return to the center page. - State is kept: the center page is never rebuilt from scratch, side pages keep their state during transitions and optionally after them.
- Controller (
FivePageController) andFivePageNavigator.of(context)for programmatic navigation, withFutures that complete when a transition ends. - Side page previews, an optional return button that avoids app bars, haptic feedback, keyboard shortcuts and reduced motion support.
- Works on Android, iOS, web, macOS, Windows and Linux. Pure Dart, no platform plugins.
Getting started #
dependencies:
five_page_navigation: ^3.0.0
import 'package:five_page_navigation/five_page_navigation.dart';
Usage #
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
final _controller = FivePageController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FivePageNavigator(
controller: _controller,
transition: const FivePageTransition.parallax(),
centerPage: const CameraPage(),
leftPage: const ChatPage(),
rightPage: const StoriesPage(),
topPage: const MemoriesPage(),
bottomPage: const SpotlightPage(),
enableLeftPageSwipeBack: true,
enableRightPageSwipeBack: true,
enableTopPageSwipeBack: true,
enableBottomPageSwipeBack: true,
onPageChanged: (page) => debugPrint('Now on ${page.name}'),
);
}
}
Every side page is optional. Pages are regular widgets, usually a Scaffold.
The navigator sizes itself to its constraints, so it can be the home of your app, the body of a tab, or a fixed-size box.
Navigating from code #
await _controller.navigateLeft(); // or navigateRight / navigateTop / navigateBottom
await _controller.navigateTo(PageType.top, animate: false);
await _controller.returnToCenter();
_controller.currentPage; // PageType.center, PageType.left, ...
_controller.isTransitioning;
Every method returns a Future<bool> that completes with true once the page is shown. Navigating from one side page to another goes through the center page.
FivePageController is a ChangeNotifier, so you can rebuild UI when the page changes:
ListenableBuilder(
listenable: _controller,
builder: (context, _) => Text(_controller.currentPage.name),
)
Inside a page you don't need a controller:
FivePageNavigator.of(context).returnToCenter();
Transitions #
Pass any of these to transition:
| Transition | Effect |
|---|---|
FivePageTransition.slide() (default) |
Both pages slide together. Optional fade with incomingOpacityStart / outgoingOpacityEnd. |
FivePageTransition.parallax() |
The new page slides over the old one, which moves slower and dims (iOS style). |
FivePageTransition.cover() |
The new page slides over the old one, which stays in place and dims. |
FivePageTransition.depth() |
Like cover, but the old page shrinks and gets rounded corners. |
FivePageTransition.reveal() |
The old page slides away and uncovers the new one. |
FivePageTransition.fade() |
Cross-fade. |
FivePageTransition.fadeThrough() |
Material fade through. |
FivePageTransition.sharedAxis() |
Material shared axis: short slide with fades. |
FivePageTransition.zoom() |
The new page zooms in, the old one zooms out. |
FivePageTransition.scale() |
The new page grows from the edge it comes from. |
FivePageTransition.cube() |
3D cube rotation. |
FivePageTransition.flip() |
3D card flip. |
FivePageTransition.rotate() |
Pages swing around a pivot. |
FivePageTransition.none() |
Instant switch. |
Most transitions take parameters, for example FivePageTransition.depth(outgoingScale: 0.8, borderRadius: 32).
Timing is controlled by transitionDuration (default 300 ms) and transitionCurve (default Curves.easeOutCubic). After a drag, only the remaining part of the duration is used. When the platform asks to reduce motion, pages switch instantly.
Transitions that scale, fade or rotate show what is behind the pages; set backgroundColor to control it.
Custom transitions #
FivePageNavigator(
transition: FivePageTransition.custom(
incomingBuilder: (context, details, child) => Opacity(
opacity: details.progress,
child: Transform.scale(scale: 0.9 + 0.1 * details.progress, child: child),
),
outgoingBuilder: (context, details, child) => FractionalTranslation(
translation: details.vector * details.progress,
child: child,
),
),
// ...
)
details contains the linear progress (0 to 1, following the finger during a drag), the direction the content moves in, its unit vector, the navigator size, and the from and to pages. You can also extend FivePageTransition directly. Keep the widget structure the same for every progress value and only change the values, so pages are not rebuilt on every frame.
Gestures #
| Parameter | Default | Description |
|---|---|---|
horizontalDetectionAreaWidth |
100 |
Width of the left and right edge zones of the center page. |
verticalDetectionAreaHeight |
200 |
Height of the top and bottom edge zones of the center page. |
swipeThreshold |
0.25 |
Fraction of the width or height a drag must cover to open a page. |
swipeBackThreshold |
swipeThreshold |
Same, for swiping a side page back. |
enableLeftPageSwipeBack (and right, top, bottom) |
false |
Allow swiping a side page back to the center. |
swipeBackDetectionAreaFraction |
0.4 |
Size of the swipe-back zone on the edge of a side page that faces the center. |
flingVelocityThreshold |
700 |
A faster release commits (or cancels) regardless of the threshold. |
canSwipeFromCenter |
null |
Return false to ignore swipes from the center page. |
prioritizeEdgeSwipes |
true |
Edge swipes win against scrollables inside the edge zones. Set to false to let same-axis scrollables win. |
swipeDeviceKinds |
all | Restrict which pointer kinds can swipe, e.g. {PointerDeviceKind.touch}. |
thresholdFeedback |
heavy |
Haptic played when a drag crosses its threshold (none, soft, medium, heavy, selection). |
A drag only starts when the pointer goes down inside an edge zone and moves in the direction of a configured page. Any other gesture goes to your page content untouched.
App bars and the back button #
All pages live inside the route that contains the navigator. While a side page is open the navigator adds a LocalHistoryEntry to that route, which gives you:
- The Android back button,
Navigator.maybePopandNavigator.pop(context)inside a side page return to the center page instead of leaving the screen. AppBarback arrow: a side page with anAppBarautomatically shows a back button that returns to the center page. Disable it withimplyAppBarBackButton: false, or per page withAppBar(automaticallyImplyLeading: false).- iOS: the swipe-back gesture of the enclosing
CupertinoPageRouteis disabled while a side page is open, so it does not fight with the navigator. - Pages without an
AppBarare fine: handle the status bar withSafeAreaas usual. - If the navigator is the first route of your app and the center page has an
AppBar, give itautomaticallyImplyLeading: false. Otherwise it would briefly show a back arrow while it slides away, because all pages share one route.
Turn the whole behaviour off with handleBackButton: false. It also does nothing when the navigator is not inside a route (for example in MaterialApp.builder).
Return button #
showReturnToCenterButton: true adds a button to each side page, on the edge that faces the center page. It is useful on desktop and web, where edge swipes with a mouse are not obvious.
returnButtonStyle: const ReturnButtonStyle(
backgroundColor: Colors.black54,
iconColor: Colors.white,
tooltip: 'Back',
elevation: 2,
respectScaffoldBars: true,
),
With respectScaffoldBars (default), the navigator measures the AppBar / CupertinoNavigationBar and bottomNavigationBar of each side page's outermost Scaffold and moves the button below or above them, so it never covers the title or the tabs. Without bars, the button only avoids the safe area.
Use customButtonBuilder: (context, onPressed, pageType) => ... to build your own button, icon to change the icon and alignment to move it.
Previews #
With showSidePagePreviews: true, dragging from the center page doesn't move the pages. Instead a small preview slides in from the edge; release past the threshold to open the page. The default preview is a labelled chip:
pagePreviewStyle: const PagePreviewStyle(
leftPageLabel: 'Chat',
rightPageLabel: 'Stories',
defaultChipBackgroundColor: Colors.black54,
previewOffsetFromEdge: 24,
),
Pass leftPagePreviewWidget (and friends) to show any widget instead.
Keyboard #
With enableKeyboardShortcuts (default true):
- Alt + ← → ↑ ↓ on the center page opens the page in that direction.
- Esc, or Alt + the arrow pointing back to the center, returns from a side page.
Shortcuts work while the focus is inside the navigator and are skipped while a text field is being edited. Clicking the navigator with a mouse moves the focus into it; set autofocus: true to make them work right away.
State and performance #
- Pages are mounted under stable keys and never rebuilt by the animation. Each page is wrapped in a
RepaintBoundary, so transitions mostly re-composite instead of repainting. - The center page stays alive (offstage, with its animations paused) while a side page is open.
- A side page is built when its transition starts and disposed after you return. Set
maintainSidePageState: trueto keep visited side pages alive. - Offstage pages are excluded from focus and from the accessibility tree.
Platform notes #
| Platform | Notes |
|---|---|
| Android | The back button returns to the center page. With gesture navigation, the system back gesture owns a narrow strip at the left and right screen edges, so keep horizontalDetectionAreaWidth wider than that (the default 100 is). |
| iOS | The home indicator sits at the very bottom edge; the default 200 pt bottom zone leaves plenty of room above it. |
| Web | Mouse and touch drags work. On mobile browsers, horizontal swipes from the very edge can trigger the browser's own back/forward navigation. |
| macOS, Windows, Linux | Drag with the mouse, use the keyboard shortcuts, or enable the return button. Haptics are skipped. |
Callbacks #
onPageChanged(PageType), onLeftPageOpened, onRightPageOpened, onTopPageOpened, onBottomPageOpened and onReturnCenterPage are called once the transition has finished.
Migrating from 2.x #
- Side pages are no longer pushed as routes.
Navigator.pop(context)inside a side page still returns to the center page, butModalRoute.of(context)in a side page is now the route of the navigator. incomingPageOpacityStartis deprecated. Usetransition: FivePageTransition.slide(incomingOpacityStart: 0.3, outgoingOpacityEnd: 0.3). The default slide no longer fades.FivePageController.navigateLeft()and friends now returnFuture<bool>.ThresholdVibrationgainednoneandselection. Exhaustiveswitchstatements over it need the new cases.- Swipe-back now uses
swipeBackThreshold(defaults toswipeThreshold) instead of a fixed 50%. - The
PagePreviewStylescale and appearance options are now applied. centerPageEntranceAnimationDurationcan now be set withoutanimateCenterPageEntranceOpacity(it used to throw an assertion error).
See the changelog for the full list.
Example #
The example app shows every transition, pages with and without app bars, previews, return buttons, swipe back and the controller. Run it on any platform:
cd example
flutter run -d chrome # or macos, windows, linux, an emulator...
