super_navigation_sidebar
A themeable, responsive app navigation sidebar for Flutter. One data model
renders in three modes the host picks from the available width: a full
expanded labelled tree with β β β connectors, an icon-only rail with
hover flyouts, and an off-canvas drawer with a scrim. Typed NavNode<T>
tree, badges, shortcut hints, header/footer slots, AppBar integration,
localization, accessibility, RTL. Zero third-party dependencies.
Features
- π Three layout modes β
expanded(full labelled tree + connectors),rail(icon-only, hover flyouts),drawer(off-canvas, scrim) β one controller drives all three. - π Auto breakpoints β
NavSidebarBreakpoints().modeFor(width)maps an available width to the right mode: expanded β₯ 1200 px Β· rail β₯ 768 px Β· drawer below. - π§ AppBar integration β
NavigationSidebarAppBarconnects directly to the controller. Drawer mode inserts a hamburger; desktop mode adds a collapse toggle. Slots:title,pageTitle,globalSearch,middle,actions, custombuilder.NavBreadcrumb<T>andNavigationSidebarSearchFieldare ready-made slot widgets. - π³ Typed
NavNode<T>tree β each node carries a strongly-typedvalue(route, screen enum, β¦);node.valuereads with no casting. - π Role derived from position β depth-0 leaf = direct; depth-0 branch = module; nested branch = group; nested leaf = item.
- β
Active-screen highlight + ancestor auto-expand β
navigate(id)sets the active row, opens every ancestor module, and (in drawer mode) dismisses the drawer. - π΄ Badges β
NavBadge(text, tone: NavBadgeTone.success/danger/muted). - β¨ Shortcut hints β two-key
['g', 'd']-style keycaps shown on hover. Visual hints only β wiring the keystroke is the host app's responsibility. - π Built-in search & filter β
searchable: trueadds a filter field that matches every level, auto-expands hits, and highlights the matched run. - β Quick Access favorites β
favoritable: trueadds per-row star toggles and a synthesized favorites band pinned at the top. - π Permission-gated nodes β
NavNode.locked+lockMessagedim a row, add a lock glyph, block navigation, and surface the reason as a tooltip. - π’ Status dots β
NavNode.status(openΒ·closedΒ·lockedΒ·attention) marks fiscal-period / ledger state before the label. - βΏ Accessibility β every interactive row is wrapped in
Semantics(button role, selected, expanded/collapsed state, lock/disable hints) and aFocuswithonKeyEventfor keyboard activation (Enter / Space). The drawer close button has an accessible label. - π Localization β
NavigationSidebarLocalizationsputs every user-facing string in one immutable class. English default; Arabic preset included. - π Deep immutability β
NavNode.childrenandNavSection.itemsare wrapped inList.unmodifiable()at construction. - π Duplicate ID validation β debug-build assertion detects duplicate
NavNode.idvalues with a clear error message. - π‘ Navigation safety β
navigate()returnsbool;onNavigateis only fired when navigation is actually applied (locked/disabled nodes are always refused). - π RTL β connectors, flyouts, drawer slide direction, and all row
padding mirror under
Directionality(textDirection: TextDirection.rtl, β¦). - π Zero dependencies β pure Flutter + Material.
Installation
dependencies:
super_navigation_sidebar: ^1.2.1
flutter pub get
Setup
Register NavigationSidebarThemeData on your MaterialApp:
import 'package:super_navigation_sidebar/super_navigation_sidebar.dart';
MaterialApp(
theme: ThemeData(
extensions: const [NavigationSidebarThemeData.light],
),
darkTheme: ThemeData(
extensions: const [NavigationSidebarThemeData.dark],
),
home: const MyApp(),
);
Quick start
1 Β· Self-contained (no external controller)
NavigationSidebar<String>(
sections: [
NavSection(title: 'Overview', items: [
NavNode(id: 'dashboard', label: 'Dashboard',
icon: Icons.dashboard_outlined, value: 'dashboard',
shortcut: ['g', 'd']),
]),
NavSection(title: 'Finance', items: [
NavNode(id: 'accounts', label: 'Accounts',
icon: Icons.menu_book_outlined, children: [
NavNode(id: 'coa', label: 'Chart of Accounts', children: [
NavNode(id: 'accounts_list', label: 'Account List',
icon: Icons.menu_book_outlined, value: 'accounts_list'),
]),
]),
]),
],
active: 'dashboard',
mode: NavSidebarMode.expanded,
onNavigate: (node) => openScreen(node.value!),
);
Note (1.2+):
NavNodeandNavSectionconstructors are no longerconst(children/items are wrapped inList.unmodifiable). Remove theconstkeyword from anyconst NavNode(β¦)orconst NavSection(β¦)call sites.
2 Β· Responsive shell with AppBar
class _AppShellState extends State<AppShell> {
final _nav = NavigationSidebarController<String>(
sections: mySections,
active: 'dashboard',
);
String _screen = 'dashboard';
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, c) {
final mode = const NavSidebarBreakpoints().modeFor(c.maxWidth);
final sidebar = NavigationSidebar<String>(
controller: _nav,
mode: mode,
header: (ctx, collapsed) => MyLogo(collapsed: collapsed),
onNavigate: (node) => setState(() => _screen = node.value!),
);
if (mode == NavSidebarMode.drawer) {
return Scaffold(
appBar: NavigationSidebarAppBar(
controller: _nav,
mode: NavSidebarMode.drawer,
pageTitle: NavBreadcrumb<String>(controller: _nav),
),
body: Stack(children: [
Positioned.fill(child: PageFor(screen: _screen)),
Positioned.fill(child: sidebar),
]),
);
}
return Row(children: [
sidebar,
Expanded(
child: Column(children: [
NavigationSidebarAppBar(
controller: _nav,
mode: mode,
showCollapseToggle: true,
pageTitle: NavBreadcrumb<String>(controller: _nav),
globalSearch: NavigationSidebarSearchField(controller: _nav),
actions: [NotificationBell(), UserAvatar()],
),
Expanded(child: PageFor(screen: _screen)),
]),
),
]);
});
}
@override
void dispose() { _nav.dispose(); super.dispose(); }
}
AppBar integration
NavigationSidebarAppBar
A PreferredSizeWidget connected to the controller. Rebuilds automatically
when the controller notifies (drawer open, collapsed flag, etc.).
| Property | Type | Description |
|---|---|---|
controller |
NavigationSidebarController |
Required. |
mode |
NavSidebarMode |
Required. Determines leading controls. |
leading |
Widget? |
Custom leading. Null β hamburger in drawer mode. |
title |
Widget? |
App / product name. |
pageTitle |
Widget? |
Breadcrumb or screen subtitle. |
globalSearch |
Widget? |
A NavigationSidebarSearchField or custom widget. |
middle |
Widget? |
Workspace switcher, env badge, etc. |
actions |
List<Widget>? |
Trailing action buttons. |
showCollapseToggle |
bool? |
Collapse β expand toggle (default true in expanded/rail). |
builder |
Function? |
Full custom content β overrides all slots. |
backgroundColor |
Color? |
Falls back to NavigationSidebarThemeData.surface. |
showBorder |
bool |
Bottom hairline border. Default true. |
height |
double? |
Bar height. Default kToolbarHeight (56 px). |
localizations |
NavigationSidebarLocalizations |
Semantic labels for built-in controls. |
NavBreadcrumb<T>
Reads controller.active and renders a βΊ-separated ancestor trail:
NavBreadcrumb<String>(
controller: nav,
separator: ' βΊ ', // default
// style / activeStyle for custom TextStyles
)
NavigationSidebarSearchField
A compact themed search field that drives controller.setQuery:
NavigationSidebarSearchField(
controller: nav,
hint: 'Search accounts, journals, reportsβ¦',
)
Localization
All user-facing strings are in NavigationSidebarLocalizations. Pass a
custom instance via NavigationSidebar.localizations and
NavigationSidebarAppBar.localizations:
// English (default β no configuration needed):
NavigationSidebar<String>(controller: nav, mode: mode)
// Arabic (pair with RTL Directionality):
Directionality(
textDirection: TextDirection.rtl,
child: NavigationSidebar<String>(
controller: nav,
mode: mode,
localizations: NavigationSidebarLocalizations.arabic,
),
)
// Partial override β only change what you need:
NavigationSidebar<String>(
controller: nav, mode: mode,
localizations: const NavigationSidebarLocalizations(
searchHint: 'Buscar navegaciΓ³nβ¦',
lockedDefault: 'Acceso restringido',
),
)
Available strings
| Field | Default (English) |
|---|---|
searchHint |
'Search navigationβ¦' |
searchEmpty |
'No matches for "{query}"' |
drawerTitle |
'Navigation' |
drawerCloseLabel |
'Close navigation' |
quickAccessTitle |
'Quick Access' |
addToQuickAccess |
'Add to Quick Access' |
removeFromQuickAccess |
'Remove from Quick Access' |
lockedDefault |
"Locked β you don't have access" |
shortcutPrefix |
'Shortcut Β· ' |
shortcutSeparator |
' then ' |
semanticExpanded |
'expanded' |
semanticCollapsed |
'collapsed' |
semanticLocked |
'locked' |
semanticDisabled |
'disabled' |
semanticToggleSidebar |
'Toggle sidebar' |
semanticOpenDrawer |
'Open navigation' |
Data model
NavSection<T>
NavSection<String>(
title: 'Finance',
items: [...], // List<NavNode<T>> β unmodifiable after construction
)
NavNode<T>
| Field | Type | Description |
|---|---|---|
id |
NavNodeId (String) |
Required. Unique across the whole sidebar (validated in debug builds). |
label |
String |
Required. Display text; search matches against this. |
icon |
IconData? |
Leading icon. |
children |
List<NavNode<T>> |
Child nodes. Unmodifiable after construction. |
value |
T? |
Strongly-typed host payload. |
badge |
NavBadge? |
Trailing badge pill. |
shortcut |
List<String>? |
Visual hint only β ['g', 'd'] renders G βΊ D. |
locked |
bool |
Permission-gate: dim, lock glyph, block nav, tooltip. |
lockMessage |
String? |
Tooltip on a locked row. |
status |
NavNodeStatus |
State dot β none/open/closed/locked/attention. |
enabled |
bool |
When false, row shown but not activatable. |
Navigation safety
navigate() returns bool: true = applied, false = refused (locked,
disabled, or id not found). onNavigate is only fired on true:
// Safe β onNavigate is never called for locked / disabled nodes:
NavigationSidebar<String>(
controller: nav,
mode: mode,
onNavigate: (node) => setState(() => _screen = node.value!),
)
// Programmatic nav with return-value check:
final ok = nav.navigate('wire'); // false if locked/disabled
Deep immutability
NavNode.children and NavSection.items are List.unmodifiable after
construction. Attempts to mutate throw UnsupportedError:
final node = NavNode(id: 'parent', label: 'Parent', children: [
NavNode(id: 'child', label: 'Child'),
]);
node.children.add(NavNode(id: 'x', label: 'X')); // throws UnsupportedError
All structural changes must go through the controller (replaceSections,
navigate, etc.) which notifies listeners correctly.
Duplicate ID validation
In debug builds the controller asserts that every NavNode.id is unique:
NavigationSidebarController<String>(sections: myBadSections);
// β AssertionError: duplicate NavNode IDs detected: [myId]
Programmatic check (for tests or host validation):
final dups = NavOps.findDuplicateIds<String>(sections);
assert(dups.isEmpty, 'Duplicate nav ids: $dups');
Accessibility
- Every interactive row:
Semantics(button role,selected,toggledexpanded/collapsed, lock/disable hints) +Focus+onKeyEvent(Enter/Space). - Drawer close button: accessible label from
localizations.drawerCloseLabel. - Locked/disabled rows:
SystemMouseCursors.forbiddencursor. - Rail items:
Tooltipwith the node label; lock glyph shown on locked nodes.
ERP / banking features
Built-in search & filter
NavigationSidebar<String>(
controller: nav,
searchable: true,
searchHint: 'Search accounts, journals, reportsβ¦',
);
Quick Access (favorites)
NavigationSidebarController<String>(
sections: sections,
favorites: {'journalEntry', 'trialBalance'},
);
NavigationSidebar<String>(controller: nav, favoritable: true);
Permission-gated nodes
NavNode(id: 'wire', label: 'Wire / SWIFT', icon: Icons.bolt_outlined,
value: 'wire', locked: true,
lockMessage: 'Requires Treasury Approver role');
Status dots
NavNode(id: 'fy25q3', label: 'FY2025 Β· Q3', value: 'fy25q3',
status: NavNodeStatus.open); // open Β· closed Β· locked Β· attention
Three modes
expanded β 248 px full tree
rail β 76 px icon column with hover flyouts
drawer β 280 px off-canvas overlay
// Place in a Stack + Positioned.fill; open via controller.openDrawer():
Stack(children: [
Positioned.fill(child: MyPage()),
Positioned.fill(
child: NavigationSidebar<String>(
controller: nav,
mode: NavSidebarMode.drawer,
),
),
])
NavigationSidebarController<T> API
Navigation
| Method | Returns | Description |
|---|---|---|
navigate(id) |
bool |
Set active, open ancestors, close drawer. false if refused. |
Expansion
| Method | Description |
|---|---|
expand(id) Β· collapse(id) Β· toggleNode(id) |
Open / close / flip a branch. |
expandAll() Β· collapseAll() |
All branches. |
Rail / drawer
| Method / setter | Description |
|---|---|
toggleCollapsed() Β· collapsed = bool |
Flip expanded β rail. |
openDrawer() Β· closeDrawer() Β· toggleDrawer() |
Drawer state. |
Data
| Method | Description |
|---|---|
replaceSections(sections) |
Hot-swap the section forest. Validates duplicates in debug. |
setQuery(q) Β· matchSet() |
Search filter. |
toggleFavorite(id) Β· setFavorites(ids) |
Quick Access. |
Reads
| Property / method | Description |
|---|---|
sections |
List<NavSection<T>> |
active / activeValue |
Active id / typed value. |
collapsed / drawerOpen / filtering |
State flags. |
isActive(id) / isExpanded(id) / ownsActive(id) |
Per-node queries. |
node(id) |
NavNode<T>? by id. |
matchSet() |
Matching ids + ancestors for the current query. |
favorites / favoriteNodes / isFavorite(id) |
Quick Access. |
Theming
ThemeData(
extensions: [
NavigationSidebarThemeData.light.copyWith(
surface: const Color(0xFFFFFFFF),
bg: const Color(0xFFF5F3EF),
border: const Color(0xFFDDD7CE),
),
],
)
Architecture
lib/
βββ super_navigation_sidebar.dart public barrel
βββ src/
βββ models.dart NavSection Β· NavNode Β· NavBadge Β· NavBadgeTone
β NavNodeRole Β· NavNodeId Β· NavNodeStatus
β NavSidebarMode Β· NavSidebarBreakpoints Β· NavOps
βββ theme.dart NavigationSidebarThemeData (ThemeExtension)
βββ localizations.dart NavigationSidebarLocalizations
βββ controller.dart NavigationSidebarController (ChangeNotifier)
β NavigationSidebarScope (InheritedNotifier)
βββ sidebar.dart NavigationSidebar<T> widget
β _NavRow Β· _RailItem Β· _RailFlyout Β· _FlyoutRow
β _NavBadgeChip Β· _ShortcutHint Β· _StarButton
βββ appbar.dart NavigationSidebarAppBar Β· NavBreadcrumb<T>
NavigationSidebarSearchField
MVC: immutable NavNode<T> models β NavigationSidebarController<T>
(ChangeNotifier) β thin NavigationSidebar<T> view β
NavigationSidebarThemeData (ThemeExtension). The controller is published to
descendant pages via NavigationSidebarScope<T> (InheritedNotifier).
Gotchas
- Host derives
mode. UseLayoutBuilder+NavSidebarBreakpoints().modeFor(width). - Role is positional. Depth determines visual treatment, not an explicit field.
valuevsid.valueis the typed host payload;idis the nav identity.- Drawer must overlay. Place in
Stack + Positioned.fill. - Register the theme extension. Without it the dark preset is used.
- Shortcuts are visual hints only. Wire the actual keystroke yourself via
Shortcuts/Actions. - No
const NavNode/NavSection(1.2+). Constructors are non-const; remove theconstkeyword. navigate()returnsbool. Code calling it in a void context compiles unchanged; only explicitvoidvariable assignment needs updating.
Additional information
- Changelog: CHANGELOG.md
- Repository: https://github.com/GeniusSystems24/super_navigation_sidebar
- Issues: https://github.com/GeniusSystems24/super_navigation_sidebar/issues
- License: MIT β see LICENSE