frost_nav_bar 0.1.0
frost_nav_bar: ^0.1.0 copied to clipboard
A premium frosted-glass bottom navigation bar with draggable indicator, smooth animations, badge support, and full RTL compatibility.
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:frost_nav_bar/frost_nav_bar.dart';
void main() {
runApp(const FrostNavBarExampleApp());
}
class FrostNavBarExampleApp extends StatefulWidget {
const FrostNavBarExampleApp({super.key});
@override
State<FrostNavBarExampleApp> createState() => _FrostNavBarExampleAppState();
}
class _FrostNavBarExampleAppState extends State<FrostNavBarExampleApp> {
ThemeMode _themeMode = ThemeMode.light;
void _toggleTheme() {
setState(() {
_themeMode =
_themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
});
}
@override
Widget build(BuildContext context) {
final app = MaterialApp(
title: 'Frost Nav Bar Demo',
debugShowCheckedModeBanner: false,
themeMode: _themeMode,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E99D3),
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E99D3),
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: ExampleHomePage(
onToggleTheme: _toggleTheme,
isDark: _themeMode == ThemeMode.dark,
),
);
// On web with wide screens, wrap in a phone-sized frame
if (!kIsWeb) return app;
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: _themeMode,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E99D3),
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E99D3),
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: Scaffold(
backgroundColor: _themeMode == ThemeMode.dark
? const Color(0xFF1A1A2E)
: const Color(0xFFF0F2F5),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Title
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
'🧊 Frost Nav Bar — Live Demo',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _themeMode == ThemeMode.dark
? Colors.white
: const Color(0xFF1E1E2E),
),
),
),
// Phone frame
Container(
width: 390,
height: 844,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(44),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 40,
spreadRadius: 2,
offset: const Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(8),
child: ClipRRect(
borderRadius: BorderRadius.circular(38),
child: app,
),
),
],
),
),
),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({
super.key,
required this.onToggleTheme,
required this.isDark,
});
final VoidCallback onToggleTheme;
final bool isDark;
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
int _currentIndex = 0;
int _cartCount = 2;
final _badgeBounceController = StreamController<void>.broadcast();
// ─── Style values ──────────────────────────────────────────────────
double _height = 56;
double _borderRadius = 28;
double _blurSigma = 16;
double _borderWidth = 0.8;
double _horizontalPadding = 12;
double _bottomPadding = 4;
double _iconSize = 22;
double _iconSelectedScale = 1.08;
double _labelFontSize = 10;
double _labelHeight = 12;
double _dotHeight = 2.5;
double _dotSelectedWidth = 18;
double _dotUnselectedWidth = 4;
double _indicatorWidthOffset = 8;
double _indicatorOverflow = 2;
double _indicatorDragScale = 1.13;
double _indicatorIdleScale = 1.05;
double _badgeSize = 16;
double _badgeOffsetTop = -7;
double _badgeOffsetEnd = -10;
double _badgeBouncePeakScale = 1.3;
double _shadowBlurRadius = 24;
double _glowBlurRadius = 20;
bool _enableHapticFeedback = true;
bool _enableDragging = true;
bool _showLabels = true;
bool _showDot = true;
bool _showIndicator = true;
@override
void dispose() {
_badgeBounceController.close();
super.dispose();
}
void _addToCart() {
setState(() => _cartCount++);
_badgeBounceController.add(null);
}
void _resetToDefaults() {
setState(() {
_height = 56;
_borderRadius = 28;
_blurSigma = 16;
_borderWidth = 0.8;
_horizontalPadding = 12;
_bottomPadding = 4;
_iconSize = 22;
_iconSelectedScale = 1.08;
_labelFontSize = 10;
_labelHeight = 12;
_dotHeight = 2.5;
_dotSelectedWidth = 18;
_dotUnselectedWidth = 4;
_indicatorWidthOffset = 8;
_indicatorOverflow = 2;
_indicatorDragScale = 1.13;
_indicatorIdleScale = 1.05;
_badgeSize = 16;
_badgeOffsetTop = -7;
_badgeOffsetEnd = -10;
_badgeBouncePeakScale = 1.3;
_shadowBlurRadius = 24;
_glowBlurRadius = 20;
_enableHapticFeedback = true;
_enableDragging = true;
_showLabels = true;
_showDot = true;
_showIndicator = true;
});
}
FrostNavBarStyle get _currentStyle => FrostNavBarStyle(
height: _height,
borderRadius: _borderRadius,
blurSigma: _blurSigma,
borderWidth: _borderWidth,
horizontalPadding: _horizontalPadding,
bottomPadding: _bottomPadding,
iconSize: _iconSize,
iconSelectedScale: _iconSelectedScale,
labelFontSize: _labelFontSize,
labelHeight: _labelHeight,
dotHeight: _dotHeight,
dotSelectedWidth: _dotSelectedWidth,
dotUnselectedWidth: _dotUnselectedWidth,
indicatorWidthOffset: _indicatorWidthOffset,
indicatorOverflow: _indicatorOverflow,
indicatorDragScale: _indicatorDragScale,
indicatorIdleScale: _indicatorIdleScale,
badgeSize: _badgeSize,
badgeOffsetTop: _badgeOffsetTop,
badgeOffsetEnd: _badgeOffsetEnd,
badgeBouncePeakScale: _badgeBouncePeakScale,
shadowBlurRadius: _shadowBlurRadius,
glowBlurRadius: _glowBlurRadius,
enableHapticFeedback: _enableHapticFeedback,
enableDragging: _enableDragging,
showLabels: _showLabels,
showDot: _showDot,
showIndicator: _showIndicator,
);
List<FrostNavItem> get _items => [
FrostNavItem(
icon: Icon(Icons.home_outlined,
size: _iconSize,
color: _currentIndex == 0
? Theme.of(context).colorScheme.primary
: null),
activeIcon: Icon(Icons.home,
size: _iconSize,
color: Theme.of(context).colorScheme.primary),
label: 'Home',
),
FrostNavItem(
icon: Icon(Icons.category_outlined,
size: _iconSize,
color: _currentIndex == 1
? Theme.of(context).colorScheme.primary
: null),
activeIcon: Icon(Icons.category,
size: _iconSize,
color: Theme.of(context).colorScheme.primary),
label: 'Categories',
),
FrostNavItem(
icon: Icon(Icons.shopping_cart_outlined,
size: _iconSize,
color: _currentIndex == 2
? Theme.of(context).colorScheme.primary
: null),
activeIcon: Icon(Icons.shopping_cart,
size: _iconSize,
color: Theme.of(context).colorScheme.primary),
label: 'Cart',
badgeCount: _cartCount,
),
FrostNavItem(
icon: Icon(Icons.person_outline,
size: _iconSize,
color: _currentIndex == 3
? Theme.of(context).colorScheme.primary
: null),
activeIcon: Icon(Icons.person,
size: _iconSize,
color: Theme.of(context).colorScheme.primary),
label: 'Profile',
),
];
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
extendBody: true,
backgroundColor: theme.colorScheme.surface,
appBar: AppBar(
title: const Text('Frost Nav Bar'),
centerTitle: true,
actions: [
IconButton(
icon: Icon(widget.isDark ? Icons.light_mode : Icons.dark_mode),
onPressed: widget.onToggleTheme,
tooltip: 'Toggle theme',
),
],
),
bottomNavigationBar: FrostNavBar(
selectedIndex: _currentIndex,
onTabChanged: (index) => setState(() => _currentIndex = index),
badgeBounceStream: _badgeBounceController.stream,
style: _currentStyle,
items: _items,
),
body: IndexedStack(
index: _currentIndex,
children: [
// ───── Home Page ─────
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.home, size: 64, color: theme.colorScheme.primary),
const SizedBox(height: 16),
Text('Home', style: theme.textTheme.headlineMedium),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _addToCart,
icon: const Icon(Icons.add_shopping_cart),
label: const Text('Add to Cart'),
),
const SizedBox(height: 8),
Text('Cart items: $_cartCount',
style: theme.textTheme.bodyLarge),
const SizedBox(height: 8),
Text('Try dragging the indicator!',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withValues(alpha: 0.5),
)),
],
),
),
// ───── Control Panel Page ─────
_buildControlPanel(theme),
// ───── Cart Page ─────
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_cart,
size: 64, color: theme.colorScheme.primary),
const SizedBox(height: 16),
Text('Cart ($_cartCount)',
style: theme.textTheme.headlineMedium),
],
),
),
// ───── Profile Page ─────
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person, size: 64, color: theme.colorScheme.primary),
const SizedBox(height: 16),
Text('Profile', style: theme.textTheme.headlineMedium),
],
),
),
],
),
);
}
// ─── Control Panel ────────────────────────────────────────────────
Widget _buildControlPanel(ThemeData theme) {
return SafeArea(
bottom: false,
child: ListView(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 8,
bottom: FrostNavBar.contentBottomInset(context, style: _currentStyle) +
16,
),
children: [
// Header
Row(
children: [
Icon(Icons.tune, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Text('Control Panel',
style: theme.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold)),
const Spacer(),
TextButton.icon(
onPressed: _resetToDefaults,
icon: const Icon(Icons.restart_alt, size: 18),
label: const Text('Reset'),
),
],
),
const SizedBox(height: 8),
// ─── Toggles ───
_buildSectionHeader('Toggles', Icons.toggle_on),
_buildSwitchTile('Show Labels', _showLabels,
(v) => setState(() => _showLabels = v)),
_buildSwitchTile('Show Dot', _showDot,
(v) => setState(() => _showDot = v)),
_buildSwitchTile('Show Indicator', _showIndicator,
(v) => setState(() => _showIndicator = v)),
_buildSwitchTile('Enable Dragging', _enableDragging,
(v) => setState(() => _enableDragging = v)),
_buildSwitchTile('Haptic Feedback', _enableHapticFeedback,
(v) => setState(() => _enableHapticFeedback = v)),
// ─── Bar ───
_buildSectionHeader('Bar', Icons.view_agenda_outlined),
_buildSlider('Height', _height, 40, 80,
(v) => setState(() => _height = v)),
_buildSlider('Border Radius', _borderRadius, 0, 40,
(v) => setState(() => _borderRadius = v)),
_buildSlider('Blur Sigma', _blurSigma, 0, 40,
(v) => setState(() => _blurSigma = v)),
_buildSlider('Border Width', _borderWidth, 0, 3,
(v) => setState(() => _borderWidth = v),
decimals: 1),
_buildSlider('H. Padding', _horizontalPadding, 0, 30,
(v) => setState(() => _horizontalPadding = v)),
_buildSlider('Bottom Padding', _bottomPadding, 0, 20,
(v) => setState(() => _bottomPadding = v)),
// ─── Shadow ───
_buildSectionHeader('Shadow & Glow', Icons.blur_on),
_buildSlider('Shadow Blur', _shadowBlurRadius, 0, 50,
(v) => setState(() => _shadowBlurRadius = v)),
_buildSlider('Glow Blur', _glowBlurRadius, 0, 40,
(v) => setState(() => _glowBlurRadius = v)),
// ─── Icon ───
_buildSectionHeader('Icon', Icons.image_outlined),
_buildSlider('Icon Size', _iconSize, 14, 36,
(v) => setState(() => _iconSize = v)),
_buildSlider('Selected Scale', _iconSelectedScale, 1.0, 1.5,
(v) => setState(() => _iconSelectedScale = v),
decimals: 2),
// ─── Label ───
_buildSectionHeader('Label', Icons.text_fields),
_buildSlider('Font Size', _labelFontSize, 6, 16,
(v) => setState(() => _labelFontSize = v)),
_buildSlider('Label Height', _labelHeight, 8, 20,
(v) => setState(() => _labelHeight = v)),
// ─── Dot ───
_buildSectionHeader('Dot Indicator', Icons.fiber_manual_record),
_buildSlider('Dot Height', _dotHeight, 1, 6,
(v) => setState(() => _dotHeight = v),
decimals: 1),
_buildSlider('Selected Width', _dotSelectedWidth, 4, 40,
(v) => setState(() => _dotSelectedWidth = v)),
_buildSlider('Unselected Width', _dotUnselectedWidth, 0, 20,
(v) => setState(() => _dotUnselectedWidth = v)),
// ─── Indicator Pill ───
_buildSectionHeader('Indicator Pill', Icons.tab_outlined),
_buildSlider('Width Offset', _indicatorWidthOffset, 0, 30,
(v) => setState(() => _indicatorWidthOffset = v)),
_buildSlider('Overflow', _indicatorOverflow, 0, 10,
(v) => setState(() => _indicatorOverflow = v)),
_buildSlider('Drag Scale', _indicatorDragScale, 1.0, 1.5,
(v) => setState(() => _indicatorDragScale = v),
decimals: 2),
_buildSlider('Idle Scale', _indicatorIdleScale, 0.8, 1.3,
(v) => setState(() => _indicatorIdleScale = v),
decimals: 2),
// ─── Badge ───
_buildSectionHeader('Badge', Icons.notifications_active),
_buildSlider('Badge Size', _badgeSize, 10, 28,
(v) => setState(() => _badgeSize = v)),
_buildSlider('Offset Top', _badgeOffsetTop, -20, 0,
(v) => setState(() => _badgeOffsetTop = v)),
_buildSlider('Offset End', _badgeOffsetEnd, -20, 0,
(v) => setState(() => _badgeOffsetEnd = v)),
_buildSlider('Bounce Scale', _badgeBouncePeakScale, 1.0, 2.0,
(v) => setState(() => _badgeBouncePeakScale = v),
decimals: 1),
const SizedBox(height: 80),
],
),
);
}
Widget _buildSectionHeader(String title, IconData icon) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(top: 20, bottom: 4),
child: Row(
children: [
Icon(icon, size: 18, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Text(title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.primary,
)),
],
),
);
}
Widget _buildSlider(
String label,
double value,
double min,
double max,
ValueChanged<double> onChanged, {
int decimals = 0,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
)),
),
Expanded(
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 3,
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 7),
overlayShape:
const RoundSliderOverlayShape(overlayRadius: 14),
),
child: Slider(
value: value.clamp(min, max),
min: min,
max: max,
onChanged: onChanged,
),
),
),
SizedBox(
width: 48,
child: Text(
value.toStringAsFixed(decimals),
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
),
],
),
);
}
Widget _buildSwitchTile(
String label, bool value, ValueChanged<bool> onChanged) {
return SwitchListTile.adaptive(
title: Text(label, style: Theme.of(context).textTheme.bodyMedium),
value: value,
onChanged: onChanged,
dense: true,
contentPadding: EdgeInsets.zero,
);
}
}