Skip to main content

frost_nav_bar 0.1.1 copy "frost_nav_bar: ^0.1.1" to clipboard
frost_nav_bar: ^0.1.1 copied to clipboard

A premium frosted-glass bottom navigation bar with draggable indicator, smooth animations, badge support, and full RTL compatibility.

example/lib/main.dart

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,
    );
  }
}
1
likes
150
points
45
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A premium frosted-glass bottom navigation bar with draggable indicator, smooth animations, badge support, and full RTL compatibility.

Repository (GitHub)
View/report issues

Topics

#navigation #bottom-navigation-bar #glassmorphism #ui #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on frost_nav_bar