bottom_shell_nav 0.2.0
bottom_shell_nav: ^0.2.0 copied to clipboard
A route-aware, persistent and adaptive bottom navigation shell for Flutter with go_router/auto_route modes, badges and custom renderers.
bottom_shell_nav #
Not another animated bottom bar. A modern navigation shell for real Flutter apps.
bottom_shell_nav gives Flutter apps a persistent bottom navigation shell:
tab state is preserved, each branch can own its own navigation stack, detail
routes keep the bottom bar visible, and the same renderer API works in core
Navigator mode, go_router mode or auto_route mode.
Why this package? #
Most bottom bar packages only draw the bar. Real apps also need tab state, nested navigation, pop-to-root behavior, SafeArea handling, badges and a clean way to work with routers.
bottom_shell_nav focuses on that complete shell:
- One persistent
Navigatorper branch in core mode. - Same-package
go_routersupport throughBottomShell.router. - Same-package
auto_routesupport throughBottomShell.autoRoute. - Lazy branch loading with
IndexedStackpersistence. - Re-tap behavior for pop-to-root.
- Selected-tab restoration and branch navigator restoration scopes.
- Public controller actions for select, reselect, can-pop and pop-to-root.
- Adaptive NavigationRail, extended rail and desktop drawer layouts.
- Material, Floating Pill and Cupertino renderers.
- Scroll-to-hide, root scroll-to-top and custom scroll registry behavior.
- Programmatic bottom bar visibility control.
- Disabled destinations, async tab guards and pending guard UX.
- Rich tab guard decisions with redirect callbacks and metadata.
- Keyboard destination shortcuts and optional haptic feedback.
- Branch lifecycle and detailed route-event callbacks for analytics.
- Badges, tooltips, semantics and 48x48 touch targets.
- Custom bottom bar, rail and drawer builders for full control.
Installation #
dependencies:
bottom_shell_nav: ^0.2.0
Core Usage #
Use BottomShell when you want the package to create and preserve one nested
Navigator per tab.
import 'package:bottom_shell_nav/bottom_shell_nav.dart';
import 'package:flutter/material.dart';
BottomShell(
appearance: BottomShellAppearance.floatingPill(),
adaptivePolicy: const AdaptiveNavigationPolicy.automatic(),
branches: [
BottomBranch(
id: 'home',
destination: const BottomDestination(
icon: Icons.home_outlined,
selectedIcon: Icons.home,
label: 'Home',
),
builder: (_) => const HomePage(),
),
BottomBranch(
id: 'search',
destination: const BottomDestination(
icon: Icons.search_outlined,
selectedIcon: Icons.search,
label: 'Search',
),
builder: (_) => const SearchPage(),
),
],
)
Inside a branch page, push normally:
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const DetailPage(),
),
);
The route is pushed inside the active branch navigator, so the bottom bar stays visible and tab state is preserved.
GoRouter Usage #
Use BottomShell.router inside StatefulShellRoute.indexedStack when
go_router owns your branch stacks.
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return BottomShell.router(
navigationShell: navigationShell,
appearance: BottomShellAppearance.floatingPill(),
destinations: const [
BottomDestination(
icon: Icons.home_outlined,
selectedIcon: Icons.home,
label: 'Home',
),
BottomDestination(
icon: Icons.person_outline,
selectedIcon: Icons.person,
label: 'Profile',
),
],
);
},
branches: [
StatefulShellBranch(routes: [
GoRoute(path: '/home', builder: (_, _) => const HomePage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: '/profile', builder: (_, _) => const ProfilePage()),
]),
],
)
AutoRoute Usage #
Use BottomShell.autoRoute inside an AutoTabsRouter builder.
AutoTabsRouter(
routes: const [
HomeRoute(),
SearchRoute(),
],
builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context);
return BottomShell.autoRoute(
tabsRouter: tabsRouter,
child: child,
appearance: BottomShellAppearance.floatingPill(),
destinations: const [
BottomDestination(
icon: Icons.home_outlined,
selectedIcon: Icons.home,
label: 'Home',
),
BottomDestination(
icon: Icons.search_outlined,
selectedIcon: Icons.search,
label: 'Search',
),
],
);
},
)
Appearance #
Material style is the default:
BottomShell(
branches: branches,
appearance: const BottomShellAppearance.material(),
)
Floating Pill is the modern preset:
BottomShell(
branches: branches,
appearance: BottomShellAppearance.floatingPill(
height: 76,
borderRadius: 34,
margin: const EdgeInsets.fromLTRB(20, 0, 20, 14),
selectedFlex: 2,
unselectedFlex: 1,
),
)
Cupertino is available for iOS-style apps:
BottomShell(
branches: branches,
appearance: const BottomShellAppearance.cupertino(),
)
Labels can be controlled with BottomLabelBehavior:
const BottomShellAppearance(
renderer: MaterialBottomBarRenderer(),
labelBehavior: BottomLabelBehavior.onlySelected,
)
Badges #
const BottomDestination(
icon: Icons.notifications_outlined,
selectedIcon: Icons.notifications,
label: 'Alerts',
badge: BranchBadge.count(12),
)
Use BranchBadge.dot() for a dot badge. Counts above 99 render as 99+.
Adaptive Navigation #
Switch from bottom bar to NavigationRail, extended rail and drawer-style
desktop navigation on larger screens:
BottomShell(
branches: branches,
adaptivePolicy: const AdaptiveNavigationPolicy.automatic(
railBreakpoint: 600,
extendedRailBreakpoint: 840,
drawerBreakpoint: 1200,
),
)
Customize adaptive surfaces without replacing the shell:
BottomShell(
branches: branches,
railBuilder: (context, state) => MyRail(state: state),
drawerBuilder: (context, state) => MyDrawer(state: state),
)
Resolve adaptive layouts yourself when coordinating desktop UI:
final layout = AdaptiveNavigationPolicy.automatic().layoutForWidth(width);
Scroll Behavior #
Hide the bottom bar while scrolling down and show it again when scrolling up:
BottomShell(
branches: branches,
scrollToHidePolicy: const ScrollToHidePolicy.enabled(),
)
Re-tapping the active root branch scrolls its primary scrollable back to top:
BottomShell(
branches: branches,
scrollToTopPolicy: const ScrollToTopPolicy.enabled(),
)
For complex pages, register a branch-specific scroll controller:
final scrollRegistry = ScrollToTopRegistry();
BottomShell(
branches: branches,
scrollToTopRegistry: scrollRegistry,
);
scrollRegistry.register(0, homeScrollController);
Control compact bottom bar visibility from nested scroll coordinators or external UI:
final visibilityController = BottomShellVisibilityController();
BottomShell(
branches: branches,
navigationVisibilityController: visibilityController,
);
visibilityController.hide();
visibilityController.show();
Body Transitions #
The default body behavior preserves branch state without animating branch
changes. Add a transition when you want tab changes to feel softer. The same
API works in core, go_router and auto_route modes:
BottomShell(
branches: branches,
bodyTransition: const BottomShellBodyTransition.fade(
duration: Duration(milliseconds: 180),
),
)
Built-in presets include fade, slide, scale and none:
BottomShell(
branches: branches,
bodyTransition: const BottomShellBodyTransition.slide(
slideOffset: Offset(0.04, 0),
),
)
For full control, provide a custom transition builder:
BottomShell(
branches: branches,
bodyTransition: BottomShellBodyTransition.custom(
builder: (context, animation, previousIndex, currentIndex, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
)
Guards and Disabled Tabs #
Disable a destination:
const BottomDestination(
icon: Icons.lock_outline,
label: 'Locked',
enabled: false,
)
Block tab selection asynchronously:
BottomShell(
branches: branches,
selectionGuardPolicy: const SelectionGuardPolicy.showPending(),
onBeforeSelect: (context, index, destination) async {
if (destination.label == 'Profile' && !isSignedIn) {
showLoginSheet(context);
return false;
}
return true;
},
)
Use rich guard decisions when you need redirect handling or analytics metadata:
BottomShell(
branches: branches,
onSelectionGuard: (context, index, destination) async {
if (destination.label == 'Profile' && !isSignedIn) {
return BottomShellGuardDecision.redirect(
reason: 'login-required',
metadata: {'target': destination.label},
redirect: () => showLoginSheet(context),
);
}
return const BottomShellGuardDecision.allow();
},
)
Controller Actions #
Use a controller when another widget needs to drive the shell:
final controller = BottomShellController();
BottomShell(
controller: controller,
branches: branches,
)
controller.select(1);
await controller.popToRoot(0);
final canPopHome = controller.canPopBranch(0);
await controller.reselectCurrent();
State Restoration #
Restore the selected tab and branch navigators:
MaterialApp(
restorationScopeId: 'app',
home: BottomShell(
restorationScopeId: 'main_shell',
branches: [
BottomBranch(
id: 'home',
restorationScopeId: 'home_branch',
destination: homeDestination,
builder: (_) => const HomePage(),
),
BottomBranch(
id: 'search',
restorationScopeId: 'search_branch',
destination: searchDestination,
builder: (_) => const SearchPage(),
),
],
),
)
Keyboard and Haptics #
Keyboard shortcuts are enabled by default for destination changes. Haptics are opt-in:
BottomShell(
branches: branches,
keyboardNavigationPolicy: const KeyboardNavigationPolicy.enabled(),
hapticFeedbackPolicy: const HapticFeedbackPolicy.selectionClick(),
)
Lifecycle and Routes #
Observe branch lifecycle and core-mode route changes:
BottomShell(
branches: branches,
onBranchEntered: (index, destination) => logTabView(destination.label),
onBranchExited: (index, destination) => logTabExit(destination.label),
onBranchBecameActiveAgain: (index, destination) => refreshIfNeeded(index),
onBranchRouteChanged: (index, route) => logRoute(route?.settings.name),
onBranchRouteEvent: (event) => logRouteEvent(event.type, event.routeName),
)
Custom Bar #
barBuilder receives the selected index, pending guard state, destinations and
selection callback.
BottomShell(
branches: branches,
barBuilder: (context, state) {
return MyBottomBar(
selectedIndex: state.selectedIndex,
destinations: state.destinations,
onSelect: state.onSelect,
);
},
)
Policies #
The defaults are tuned for app navigation:
BottomShell(
branches: branches,
persistence: const BranchPersistencePolicy.indexedStack(
lazyLoadBranches: true,
),
reTapBehavior: const ReTapBehavior.popToRoot(),
safeAreaPolicy: const SafeAreaPolicy(),
keyboardPolicy: const KeyboardPolicy.hideNavigationBar(),
adaptivePolicy: const AdaptiveNavigationPolicy.automatic(),
bodyTransition: const BottomShellBodyTransition.none(),
keyboardNavigationPolicy: const KeyboardNavigationPolicy.enabled(),
hapticFeedbackPolicy: const HapticFeedbackPolicy.disabled(),
selectionGuardPolicy: const SelectionGuardPolicy.showPending(),
)
Example #
Run the core Navigator demo:
cd example
flutter run -t lib/main.dart
Run the go_router demo:
cd example
flutter run -t lib/go_router_example.dart
Run the auto_route demo:
cd example
flutter run -t lib/auto_route_example.dart
Roadmap #
- Glass renderer only after performance and platform testing.
- Adaptive navigation rail/drawer refinements.
- Cupertino route-aware renderer improvements.
License #
MIT