adaptive_scaffold_plus
A production-ready, community-maintained replacement for the discontinued
flutter_adaptive_scaffold package. Drop it into any Flutter app to get
navigation that automatically adapts to screen size, following Material 3
guidelines — with zero manual breakpoint logic.
| Screen width | Navigation shown |
|---|---|
< 600px |
Bottom NavigationBar (docked) or a floating, frosted-glass pill bar |
600–1200px |
NavigationRail (side) |
> 1200px |
Permanent NavigationDrawer (full) |
Resize the window / rotate the device and the transition animates automatically.
Install
dependencies:
adaptive_scaffold_plus: ^1.0.5
import 'package:adaptive_scaffold_plus/adaptive_scaffold_plus.dart';
Quick start
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AdaptiveScaffoldPlus(
destinations: const [
AdaptiveDestination(icon: Icons.home_outlined, selectedIcon: Icons.home, label: 'Home'),
AdaptiveDestination(icon: Icons.search_outlined, selectedIcon: Icons.search, label: 'Search'),
AdaptiveDestination(icon: Icons.person_outline, selectedIcon: Icons.person, label: 'Profile'),
],
body: (index) => pages[index],
),
);
}
}
That's it — AdaptiveScaffoldPlus picks the right navigation widget for the
current screen size and keeps body(selectedIndex) in sync with whichever
destination is selected.
Floating, frosted-glass navigation bar
By default the small-screen layout uses a standard, opaque NavigationBar
docked to the bottom edge (AdaptiveNavigationStyle.docked). Set
navigationStyle: AdaptiveNavigationStyle.floating to switch to a
pill-shaped, translucent bar that floats above your content with blurred
background — the modern look used by apps like Instagram.
AdaptiveScaffoldPlus(
destinations: destinations,
navigationStyle: AdaptiveNavigationStyle.floating,
body: (index) => pages[index],
)
The bar automatically adapts its icon color to stay legible over both light and dark content, and shows an animated pill highlight behind the selected icon.
Tuning the floating bar
AdaptiveScaffoldPlus(
destinations: destinations,
navigationStyle: AdaptiveNavigationStyle.floating,
navigationBackgroundColor: Colors.black, // tint of the glass surface
floatingNavigationMargin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
floatingNavigationBlur: 30, // frosted-glass blur strength
floatingNavigationOpacity: 0.45, // 0 = fully see-through, 1 = opaque
floatingNavigationHeight: 56,
body: (index) => pages[index],
)
| Parameter | Default | Effect |
|---|---|---|
navigationBackgroundColor |
white (light theme) / black (dark) | Tint color of the glass surface |
floatingNavigationMargin |
EdgeInsets.fromLTRB(16, 0, 16, 16) |
Space between the bar and the screen edges |
floatingNavigationBlur |
30 |
BackdropFilter blur sigma (frosted-glass strength) |
floatingNavigationOpacity |
0.45 |
Alpha of the tint color, 0–1 |
floatingNavigationHeight |
56 |
Height of the bar itself |
You can also use the bar standalone, outside of AdaptiveScaffoldPlus:
Scaffold(
extendBody: true, // lets content scroll behind the floating bar
body: MyContent(),
bottomNavigationBar: SafeArea(
minimum: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: FloatingNavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: (i) => setState(() => selectedIndex = i),
),
),
)
Destinations
Each nav item is an AdaptiveDestination — it maps to a NavigationBar /
FloatingNavigationBar item on small screens, a NavigationRail item on
medium screens, and a NavigationDrawer item on large screens.
AdaptiveDestination(
icon: Icons.favorite_outline,
selectedIcon: Icons.favorite, // shown when selected; falls back to `icon`
label: 'Favourites',
tooltip: 'Your favourites', // shown on long-press / hover
badge: Text('3'), // optional badge/notification content
)
At least 2 destinations are required.
Full AdaptiveScaffoldPlus reference
AdaptiveScaffoldPlus(
// Required
destinations: [...], // List<AdaptiveDestination>, min 2
body: (index) => pages[index], // Widget Function(int selectedIndex)
// Layout
secondaryBody: (index) => detail[index],// optional second panel (list-detail pattern, large screens)
appBar: AppBar(title: const Text('My App')),
floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
backgroundColor: Colors.white,
resizeToAvoidBottomInset: true,
// Selection
initialIndex: 0,
onDestinationSelected: (index) => print('selected $index'),
// Breakpoints
breakpoints: const AdaptiveBreakpoints(small: 600, large: 1200),
// Medium screen (NavigationRail)
railLabelType: NavigationRailLabelType.all,
extendedRail: false,
// Medium/large screen extras
navigationHeader: const Text('MY APP'), // shown above destinations
navigationFooter: const Text('v1.0.5'), // shown below destinations
showNavigationDivider: true,
// Styling shared across nav types
navigationBackgroundColor: Colors.white,
transitionDuration: const Duration(milliseconds: 200),
// Small-screen navigation style (see "Floating" section above)
navigationStyle: AdaptiveNavigationStyle.docked, // or .floating
floatingNavigationMargin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
floatingNavigationBlur: 30,
floatingNavigationOpacity: 0.45,
floatingNavigationHeight: 56,
)
Custom breakpoints
AdaptiveScaffoldPlus(
breakpoints: const AdaptiveBreakpoints(small: 500, large: 1100),
destinations: [...],
body: (index) => pages[index],
)
You can also check the current size manually anywhere in your widget tree:
final size = Breakpoints.of(context); // ScreenSize.small / .medium / .large
if (Breakpoints.isSmall(context)) { ... }
List-detail pattern (large screens)
AdaptiveScaffoldPlus(
destinations: [...],
body: (index) => MyListView(),
secondaryBody: (index) => MyDetailView(), // shown beside body only on large screens
)
Low-level layout: AdaptiveLayout + SlotLayout
For full control over what renders in each screen region (instead of the
opinionated nav-bar/rail/drawer behavior of AdaptiveScaffoldPlus), compose
AdaptiveLayout directly with per-breakpoint SlotLayouts:
AdaptiveLayout(
body: SlotLayout(
config: {
ScreenSize.small: SlotLayoutConfig.from(child: MobilePage()),
ScreenSize.large: SlotLayoutConfig.from(child: DesktopPage()),
},
),
primaryNavigation: SlotLayout(
config: {
ScreenSize.medium: SlotLayoutConfig.from(child: MyNavRail()),
ScreenSize.large: SlotLayoutConfig.from(child: MyDrawer()),
},
),
topNavigation: SlotLayout(
config: {
ScreenSize.small: SlotLayoutConfig.from(child: AppBar(title: const Text('App'))),
},
),
)
Slots: body, secondaryBody, primaryNavigation, secondaryNavigation,
topNavigation, bottomNavigation — each is an optional SlotLayout that
picks a child per ScreenSize, with a fade transition by default
(override via SlotLayoutConfig.from(inAnimation: ..., outAnimation: ...)).
Try it locally
A runnable demo lives in example/lib/main.dart:
cd example
flutter run -d chrome # or any connected device/desktop target
Resize the window (or use responsive mode in Chrome DevTools) to see the layout switch between bottom bar, rail, and drawer live.
License
See LICENSE.
Libraries
- adaptive_scaffold_plus
- adaptive_scaffold_plus
- main