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.

Floating, frosted-glass navigation bar in light and dark theme

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