bottom_shell_nav 0.4.3 copy "bottom_shell_nav: ^0.4.3" to clipboard
bottom_shell_nav: ^0.4.3 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
Pill selection Material labels Center highlight
Alert emphasis Minimal accent Bubble selection
Alert emphasis Minimal accent Bubble selection
Profile pill Tasks pill Dark glow
Profile pill Tasks pill Dark glow
Neumorphic
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 Navigator per branch in core mode.
  • Same-package go_router support through BottomShell.router.
  • Same-package auto_route support through BottomShell.autoRoute.
  • Lazy branch loading with IndexedStack persistence.
  • 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.3.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(),
)

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

7
likes
160
points
67
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A route-aware, persistent and adaptive bottom navigation shell for Flutter with go_router/auto_route modes, badges and custom renderers.

Repository (GitHub)
View/report issues

Topics

#bottom-navigation #navigation-shell #nested-navigation #go-router #material3

License

MIT (license)

Dependencies

auto_route, flutter, go_router

More

Packages that depend on bottom_shell_nav