snap_nav_bar 0.1.0
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(),
),
),
),
),
],
);
}
}