bottom_shell_nav 0.4.4
bottom_shell_nav: ^0.4.4 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.
Screenshots #
A few renderer and selection states from the example app:
| Pill selection | Material labels | Center highlight |
|---|---|---|
![]() |
![]() |
![]() |
| Alert emphasis | Minimal accent | Bubble selection |
|---|---|---|
![]() |
![]() |
![]() |
| Profile pill | Tasks pill | Dark glow |
|---|---|---|
![]() |
![]() |
![]() |
| Neumorphic |
|---|
![]() |
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, Cupertino, Curved, GNav, Dot Indicator, Water Drop, Flashy, Bubble, Convex, Sliding, Glow and Neumorphic 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.4.4
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(),
)
Additional presets are available for more opinionated UI styles:
Curved / Notched #
A bar with a smooth concave notch — the selected icon floats in a raised circle above the bar surface:
BottomShellAppearance.curved(
height: 62,
curveDepth: 30, // how deep the notch dips
curveWidth: 75, // horizontal notch opening
fabSize: 56, // floating circle diameter
fabOffset: -18, // negative = higher above bar
)
GNav (Google Pill) #
Selected item expands into a pill shape with icon + label, Google-style:
BottomShellAppearance.gNav(
height: 64,
gap: 8, // space between icon and label
tabBorderRadius: 100, // pill roundness
tabPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
)
Dot Indicator #
Minimal style — a small animated dot appears below the selected icon:
BottomShellAppearance.dotIndicator(
height: 64,
dotSize: 5,
dotSpacing: 4,
)
Water Drop #
The selected icon drops into a colored circle with a top indicator line:
BottomShellAppearance.waterDrop(
height: 68,
dropHeight: 22,
dropRadius: 20,
)
Flashy #
Icon slides up, label fades in from below:
BottomShellAppearance.flashy(
height: 60,
iconShift: -6, // how far icon moves up
)
Bubble / Expanding #
Selected item expands into a background pill with icon + label:
BottomShellAppearance.bubble(
height: 64,
bubbleBorderRadius: 20,
bubblePadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
)
Convex FAB #
A raised center button (FAB-like) with the rest evenly distributed:
BottomShellAppearance.convex(
height: 56,
centerIndex: -1, // -1 = auto middle
convexSize: 54,
convexElevation: 20,
convexBorderRadius: 27,
)
Sliding Indicator #
A background highlight slides horizontally behind the selected item:
BottomShellAppearance.sliding(
height: 64,
indicatorHeight: 44,
indicatorBorderRadius: 14,
indicatorMargin: const EdgeInsets.symmetric(horizontal: 6),
)
Glow / Neon #
Dark floating pill with a colored glow/neon effect on the selected icon:
BottomShellAppearance.glow(
height: 64,
borderRadius: 40,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 16),
backgroundColor: const Color(0xFF1E1E2C), // inner bar
surfaceColor: const Color(0xFFE9E6F2), // outer card
surfacePadding: const EdgeInsets.all(6),
glowRadius: 24,
glowSpread: 8,
glowOpacity: 0.55,
)
Custom Renderer #
For full control, implement BottomBarRenderer and pass it directly:
BottomShellAppearance(
renderer: MyCustomRenderer(),
labelBehavior: BottomLabelBehavior.onlySelected,
animationStyle: const BottomBarAnimationStyle(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOutCubic,
),
)
Animation Timing #
All renderers share a common animation style that controls duration and curve:
BottomShellAppearance.flashy(
animationStyle: const BottomBarAnimationStyle(
duration: Duration(milliseconds: 350),
curve: Curves.elasticOut,
),
)
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 #
The example app includes a live style switcher — a dropdown at the top of every page lets you switch between all 12 built-in bottom bar presets at runtime.
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
What the example demonstrates #
| Feature | How to see it |
|---|---|
| Persistent tab state | Increment the counter, switch tabs, come back |
| Nested navigation | Tap any list item — pushes inside the branch |
| Pop-to-root | Tap the already-selected tab icon |
| Badges | "Alerts" tab shows a count badge |
| Scroll-to-hide | Scroll any list down — the bar hides |
| Body transitions | Fade animation on tab switch |
| Adaptive layout | Resize (desktop) or rotate (tablet) |
| Live style switching | Use the "Bar Style" dropdown |
Minimal quick-start #
import 'package:bottom_shell_nav/bottom_shell_nav.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BottomShell(
appearance: BottomShellAppearance.glow(
backgroundColor: const Color(0xFF1A1B26),
),
branches: [
BottomBranch(
id: 'home',
destination: const BottomDestination(
icon: Icons.home_outlined,
selectedIcon: Icons.home,
label: 'Home',
),
builder: (_) => const Center(child: Text('Home')),
),
BottomBranch(
id: 'search',
destination: const BottomDestination(
icon: Icons.search,
label: 'Search',
),
builder: (_) => const Center(child: Text('Search')),
),
BottomBranch(
id: 'profile',
destination: const BottomDestination(
icon: Icons.person_outline,
selectedIcon: Icons.person,
label: 'Profile',
),
builder: (_) => const Center(child: Text('Profile')),
),
],
),
);
}
}
Roadmap #
- Glass renderer only after performance and platform testing.
- Adaptive navigation rail/drawer refinements.
- Cupertino route-aware renderer improvements.
License #
MIT










