glass_bottom_bar 0.0.1
glass_bottom_bar: ^0.0.1 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')),
),
),
);
}