glass_bottom_bar 0.0.2 copy "glass_bottom_bar: ^0.0.2" to clipboard
glass_bottom_bar: ^0.0.2 copied to clipboard

A floating frosted-glass bottom navigation bar with scroll-aware shrink animation, badge support, active icon swap, and an iOS 26 Liquid Glass aesthetic. Dark/light theme adaptive with no extra depend [...]

example/lib/main.dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme_cubit.dart';
import 'theme_state.dart';

import 'package:glass_bottom_bar/glass_bottom_bar.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ThemeCubit(),
      child: BlocBuilder<ThemeCubit, ThemeState>(
        builder: (context, state) {
          return MaterialApp(
            debugShowCheckedModeBanner: false,
            theme: ThemeData(
              brightness: Brightness.light,
              useMaterial3: true,
              scaffoldBackgroundColor: const Color(0xFFF8F9FA),
            ),
            darkTheme: ThemeData(
              brightness: Brightness.dark,
              useMaterial3: true,
              scaffoldBackgroundColor: const Color(0xFF0D0D0D),
            ),
            themeMode: state.themeMode,
            home: const GlassDemoScreen(),
          );
        },
      ),
    );
  }
}

class GlassDemoScreen extends StatefulWidget {
  const GlassDemoScreen({super.key});

  @override
  State<GlassDemoScreen> createState() => _GlassDemoScreenState();
}

class _GlassDemoScreenState extends State<GlassDemoScreen> {
  int _index = 0;

  late final List<ScrollController> _controllers = [
    ScrollController(),
    PageController(),
    ScrollController(),
    ScrollController(),
    ScrollController(),
  ];

  @override
  void dispose() {
    for (var controller in _controllers) {
      controller.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;

    return Scaffold(
      extendBody: true,
      body: Stack(
        children: [
          // --- LIQUID BACKGROUND LAYER ---
          Positioned.fill(
            child: Container(
              color: isDark ? const Color(0xFF0D0D0D) : Colors.white,
            ),
          ),
          // Ambient color blobs — this is what the glass actually refracts/
          // glows against. Clipped to their own circular bounds so the blur
          // doesn't bleed outside the blob shape.
          Positioned(
            top: -100,
            right: -50,
            child: _BlurredBlob(
              color: isDark
                  ? Colors.purple.withValues(alpha: 0.3)
                  : Colors.purple.withValues(alpha: 0.2),
              size: 400,
            ),
          ),
          Positioned(
            bottom: -50,
            left: -100,
            child: _BlurredBlob(
              color: isDark
                  ? Colors.blue.withValues(alpha: 0.2)
                  : Colors.blue.withValues(alpha: 0.15),
              size: 500,
            ),
          ),

          // --- CONTENT LAYER ---
          IndexedStack(
            index: _index,
            children: [
              _HomeTab(scrollController: _controllers[0]),
              _ReelsTab(scrollController: _controllers[1] as PageController),
              _MessagesTab(scrollController: _controllers[2]),
              _SearchTab(scrollController: _controllers[3]),
              _ProfileTab(scrollController: _controllers[4]),
            ],
          ),
        ],
      ),
      bottomNavigationBar: GlassBottomBar(
        scrollController: _controllers[_index],
        height: 82,
        blur: 25,
        // Increased blur for more "liquid" feel
        margin: const EdgeInsets.fromLTRB(12, 0, 12, 30),
        currentIndex: _index,
        onTap: (i) => setState(() => _index = i),
        items: const [
          GlassBarItem(
            icon: Icons.home_rounded,
            activeIcon: Icons.home_rounded,
          ),
          GlassBarItem(
            icon: Icons.search_rounded,
            activeIcon: Icons.search_rounded,
          ),
          GlassBarItem(icon: Icons.explore_rounded, badge: true),
          // GlassBarItem(icon: Icons.explore_rounded),
          GlassBarItem(
            icon: Icons.account_circle_outlined,
            activeIcon: Icons.account_circle,
          ),
          GlassBarItem(icon: Icons.notifications_rounded),
        ],
      ),
    );
  }
}

class _BlurredBlob extends StatelessWidget {
  final Color color;
  final double size;

  const _BlurredBlob({required this.color, required this.size});

  @override
  Widget build(BuildContext context) {
    // ClipOval contains the blur to the blob's circular bounds — without
    // this, BackdropFilter's blur can bleed past the widget's edges.
    return ClipOval(
      child: SizedBox(
        width: size,
        height: size,
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 80, sigmaY: 80),
          child: Container(
            decoration: BoxDecoration(shape: BoxShape.circle, color: color),
          ),
        ),
      ),
    );
  }
}

class _GlassAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String title;
  final Widget? trailing;

  const _GlassAppBar({required this.title, this.trailing});

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return ClipRect(
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
        child: Container(
          padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
          color: isDark
              ? Colors.black.withValues(alpha: 0.2)
              : Colors.white.withValues(alpha: 0.3),
          height: kToolbarHeight + MediaQuery.of(context).padding.top,
          child: Row(
            children: [
              const SizedBox(width: 20),
              Text(
                title,
                style: const TextStyle(
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const Spacer(),
              if (trailing != null) trailing!,
              const SizedBox(width: 10),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

class _HomeTab extends StatelessWidget {
  final ScrollController scrollController;

  const _HomeTab({required this.scrollController});

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        CustomScrollView(
          controller: scrollController,
          slivers: [
            SliverToBoxAdapter(
              child: SizedBox(
                height:
                    kToolbarHeight + MediaQuery.of(context).padding.top + 20,
              ),
            ),
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) => _LiquidPostCard(index: index),
                childCount: 10,
              ),
            ),
            const SliverPadding(padding: EdgeInsets.only(bottom: 120)),
          ],
        ),
        Positioned(
          top: 0,
          left: 0,
          right: 0,
          child: _GlassAppBar(
            title: 'Instagram',
            trailing: IconButton(
              icon: const Icon(Icons.brightness_6),
              onPressed: () => context.read<ThemeCubit>().toggle(),
            ),
          ),
        ),
      ],
    );
  }
}

class _LiquidPostCard extends StatelessWidget {
  final int index;

  const _LiquidPostCard({required this.index});

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Container(
      margin: const EdgeInsets.fromLTRB(16, 0, 16, 20),
      height: 350,
      decoration: BoxDecoration(
        color: isDark
            ? Colors.white.withValues(alpha: 0.05)
            : Colors.white.withValues(alpha: 0.4),
        borderRadius: BorderRadius.circular(24),
        border: Border.all(
          color: isDark
              ? Colors.white.withValues(alpha: 0.1)
              : Colors.white.withValues(alpha: 0.6),
        ),
      ),
      child: Center(
        child: Icon(
          Icons.image_outlined,
          size: 40,
          color: isDark ? Colors.white24 : Colors.black12,
        ),
      ),
    );
  }
}

// Minimal implementation for other tabs to keep focus on UI
class _ReelsTab extends StatelessWidget {
  final PageController scrollController;

  const _ReelsTab({required this.scrollController});

  @override
  Widget build(BuildContext context) => PageView.builder(
    controller: scrollController,
    scrollDirection: Axis.vertical,
    itemBuilder: (context, i) => Center(child: Text('Reel $i')),
  );
}

class _MessagesTab extends StatelessWidget {
  final ScrollController scrollController;

  const _MessagesTab({required this.scrollController});

  @override
  Widget build(BuildContext context) => ListView.builder(
    controller: scrollController,
    itemBuilder: (context, i) => ListTile(title: Text('Message $i')),
  );
}

class _SearchTab extends StatelessWidget {
  final ScrollController scrollController;

  const _SearchTab({required this.scrollController});

  @override
  Widget build(BuildContext context) => GridView.builder(
    controller: scrollController,
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3,
    ),
    itemBuilder: (context, i) => Container(
      margin: const EdgeInsets.all(2),
      color: Colors.blueGrey.withValues(alpha: 0.1),
    ),
  );
}

class _ProfileTab extends StatelessWidget {
  final ScrollController scrollController;

  const _ProfileTab({required this.scrollController});

  @override
  Widget build(BuildContext context) => SingleChildScrollView(
    controller: scrollController,
    child: Column(
      children: List.generate(
        20,
        (i) => ListTile(title: Text('Profile Item $i')),
      ),
    ),
  );
}
0
likes
140
points
145
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A floating frosted-glass bottom navigation bar with scroll-aware shrink animation, badge support, active icon swap, and an iOS 26 Liquid Glass aesthetic. Dark/light theme adaptive with no extra dependencies. Created by Divya Soranga.

Repository (GitHub)
View/report issues

Topics

#ui #navigation #bottom-bar #glass #widget

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on glass_bottom_bar