snap_nav_bar 0.1.0 copy "snap_nav_bar: ^0.1.0" to clipboard
snap_nav_bar: ^0.1.0 copied to clipboard

A frosted, gesture-driven bottom navigation bar with loose swipe-to-preview paging, snap-on-release physics, tap-to-centre and haptic feedback.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:snap_nav_bar/snap_nav_bar.dart';

void main() => runApp(const ExampleApp());

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'snap_nav_bar example',
      theme: ThemeData(brightness: Brightness.dark, useMaterial3: true),
      home: const _Launcher(),
    );
  }
}

/// A tiny menu to open each demo, so both fit in one runnable app.
class _Launcher extends StatelessWidget {
  const _Launcher();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('snap_nav_bar')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            FilledButton(
              onPressed: () => Navigator.of(context).push(
                MaterialPageRoute<void>(builder: (_) => const _DefaultDemo()),
              ),
              child: const Text('Default bar'),
            ),
            const SizedBox(height: 16),
            FilledButton.tonal(
              onPressed: () => Navigator.of(context).push(
                MaterialPageRoute<void>(builder: (_) => const _CustomDemo()),
              ),
              child: const Text('Customised bar'),
            ),
          ],
        ),
      ),
    );
  }
}

/// Everything at its default value — the out-of-the-box look and feel.
class _DefaultDemo extends StatelessWidget {
  const _DefaultDemo();

  @override
  Widget build(BuildContext context) {
    return _BackButtonOverlay(
      child: SnapNavShell(
        items: const [
          SnapNavItem(
            icon: Icon(Icons.home_outlined),
            activeIcon: Icon(Icons.home_rounded),
            label: 'Home',
            page: _DemoPage(color: Color(0xFF3B82F6), label: 'Home'),
          ),
          SnapNavItem(
            icon: Icon(Icons.search_outlined),
            activeIcon: Icon(Icons.search_rounded),
            label: 'Search',
            page: _DemoPage(color: Color(0xFF10B981), label: 'Search'),
          ),
          SnapNavItem(
            icon: Icon(Icons.grid_view_outlined),
            activeIcon: Icon(Icons.grid_view_rounded),
            label: 'Library',
            page: _DemoPage(color: Color(0xFFF59E0B), label: 'Library'),
          ),
          SnapNavItem(
            icon: Icon(Icons.person_outline),
            activeIcon: Icon(Icons.person_rounded),
            label: 'Profile',
            page: _DemoPage(color: Color(0xFFEF4444), label: 'Profile'),
          ),
        ],
      ),
    );
  }
}

/// A heavily customised bar: 6 tabs, a solid coloured floating pill with no
/// blur, a custom indicator, wider tab spacing and a shadow — to prove the
/// parameters actually take effect.
class _CustomDemo extends StatelessWidget {
  const _CustomDemo();

  static const _colors = [
    Color(0xFF6366F1),
    Color(0xFF8B5CF6),
    Color(0xFFEC4899),
    Color(0xFFF43F5E),
    Color(0xFFF97316),
    Color(0xFF14B8A6),
  ];

  static const _icons = [
    Icons.dashboard_rounded,
    Icons.explore_rounded,
    Icons.favorite_rounded,
    Icons.notifications_rounded,
    Icons.chat_bubble_rounded,
    Icons.settings_rounded,
  ];

  static const _labels = [
    'Board',
    'Explore',
    'Likes',
    'Alerts',
    'Chat',
    'Settings',
  ];

  @override
  Widget build(BuildContext context) {
    return _BackButtonOverlay(
      child: SnapNavShell(
        items: [
          for (var i = 0; i < _colors.length; i++)
            SnapNavItem(
              icon: Icon(_icons[i], color: Colors.white),
              label: _labels[i],
              page: _DemoPage(color: _colors[i], label: _labels[i]),
            ),
        ],
        // Surface: solid indigo floating pill, no frost.
        backgroundColor: const Color(0xFF1E1B4B),
        blurSigma: null,
        cornerRadius: 28,
        cornerSmoothing: 0.6,
        horizontalMargin: 16,
        bottomMargin: 16,
        height: 72,
        border: Border.all(color: Colors.white24),
        boxShadow: const [
          BoxShadow(
            color: Color(0x66000000),
            blurRadius: 24,
            offset: Offset(0, 12),
          ),
        ],
        // Motion & feel.
        tabSpacing: 60,
        tapToCenterDuration: const Duration(milliseconds: 420),
        tapToCenterCurve: Curves.easeInOutCubic,
        inactiveOpacity: 0.35,
        inactiveScale: 0.8,
        // A custom indicator instead of the red arrow.
        indicator: const _DotIndicator(),
        indicatorSize: const Size(10, 10),
        indicatorColor: Colors.amber,
        // Swap the built-in effects for heavier ones to prove the hook works.
        hapticFeedback: (moment) {
          switch (moment) {
            case SnapNavHaptic.tabCrossed:
              HapticFeedback.mediumImpact();
            case SnapNavHaptic.settled:
              HapticFeedback.heavyImpact();
          }
        },
      ),
    );
  }
}

class _DotIndicator extends StatelessWidget {
  const _DotIndicator();

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: Colors.amber,
        shape: BoxShape.circle,
      ),
    );
  }
}

class _DemoPage extends StatelessWidget {
  final Color color;
  final String label;

  const _DemoPage({required this.color, required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      alignment: Alignment.center,
      child: Text(
        label,
        style: const TextStyle(
          color: Colors.white,
          fontSize: 40,
          fontWeight: FontWeight.w700,
        ),
      ),
    );
  }
}

/// Overlays a back button on a full-screen demo (the shell is a bare
/// scaffold with no app bar).
class _BackButtonOverlay extends StatelessWidget {
  final Widget child;

  const _BackButtonOverlay({required this.child});

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned.fill(child: child),
        Positioned(
          top: 0,
          left: 8,
          child: SafeArea(
            child: Material(
              color: Colors.black26,
              shape: const CircleBorder(),
              child: IconButton(
                icon: const Icon(Icons.arrow_back, color: Colors.white),
                onPressed: () => Navigator.of(context).maybePop(),
              ),
            ),
          ),
        ),
      ],
    );
  }
}
1
likes
150
points
6
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A frosted, gesture-driven bottom navigation bar with loose swipe-to-preview paging, snap-on-release physics, tap-to-centre and haptic feedback.

Topics

#navigation #bottom-navigation #page-view #gestures #ui

License

MIT (license)

Dependencies

figma_squircle, flutter

More

Packages that depend on snap_nav_bar