ios_liquid_glass 0.8.0
ios_liquid_glass: ^0.8.0 copied to clipboard
Native iOS 26 Liquid Glass (UIGlassEffect and UITabBarController) for Flutter, with plain no-effect fallbacks on all other platforms.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:ios_liquid_glass/ios_liquid_glass.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await LiquidGlassNativeTabShell.instance.initialize();
runApp(const LiquidGlassExampleApp());
}
class LiquidGlassExampleApp extends StatelessWidget {
const LiquidGlassExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Liquid Glass Kit',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF015488),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
/// The four app tabs. On iOS 26+ they become Apple's real `UITabBar` (true
/// Liquid Glass); elsewhere a plain Flutter bar with the same destinations.
/// Rebuilt whenever the badge toggles (see `configureTabs`).
List<LiquidGlassNativeTab> _buildNativeTabs({required bool badge}) => [
const LiquidGlassNativeTab(title: 'Home', systemImage: 'house', selectedSystemImage: 'house.fill'),
const LiquidGlassNativeTab(title: 'Search', systemImage: 'magnifyingglass'),
LiquidGlassNativeTab(
title: 'Favorites',
systemImage: 'heart',
selectedSystemImage: 'heart.fill',
badge: badge ? '3' : null,
),
const LiquidGlassNativeTab(title: 'Profile', systemImage: 'person.circle', selectedSystemImage: 'person.circle.fill'),
];
const List<LiquidGlassNavItem> _fallbackNavItems = [
LiquidGlassNavItem(label: 'Home', icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home)),
LiquidGlassNavItem(label: 'Search', icon: Icon(Icons.search_outlined), activeIcon: Icon(Icons.search)),
LiquidGlassNavItem(label: 'Favorites', icon: Icon(Icons.favorite_outline), activeIcon: Icon(Icons.favorite)),
LiquidGlassNavItem(label: 'Profile', icon: Icon(Icons.person_outline), activeIcon: Icon(Icons.person)),
];
const LiquidGlassStyle _clearStyle = LiquidGlassStyle(
nativeVariant: NativeGlassVariant.clear,
);
const LiquidGlassStyle _tintedStyle = LiquidGlassStyle(
nativeTintColor: Color(0xFF0A84FF),
plainColor: Color(0xFF0A84FF),
);
const LiquidGlassStyle _darkStyle = LiquidGlassStyle(
nativeTintColor: Colors.black,
plainColor: Color(0xFF1C1C1E),
);
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
static const LiquidGlassTheme _theme = LiquidGlassTheme();
int _currentIndex = 0;
bool _barVisible = true;
bool _badgeOn = true;
final LiquidGlassNativeTabShell _shell = LiquidGlassNativeTabShell.instance;
bool get _usesNativeTabBar => LiquidGlassPlatform.supportsNativeTabShell;
List<LiquidGlassNativeTab> get _tabs => _buildNativeTabs(badge: _badgeOn);
@override
void initState() {
super.initState();
if (_usesNativeTabBar) {
_shell.onTabSelected = (index) {
setState(() => _currentIndex = index);
};
_shell.ensureInstalled(_tabs, theme: _theme);
}
}
@override
void dispose() {
if (_usesNativeTabBar) {
_shell.onTabSelected = null;
_shell.uninstallShell();
}
super.dispose();
}
void _showTapped(String label) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text('$label tapped'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
Text _sectionTitle(String text) => Text(
text.toUpperCase(),
style: const TextStyle(
color: Colors.white70,
fontWeight: FontWeight.w700,
fontSize: 13,
letterSpacing: 1.4,
shadows: [Shadow(color: Colors.black26, blurRadius: 6)],
),
);
Widget _controlButton(String label, VoidCallback onPressed) =>
LiquidGlassButton(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 18),
style: _darkStyle,
onPressed: onPressed,
child: Text(label, style: _buttonLabel.copyWith(fontSize: 14)),
);
/// Soft shadow keeps labels readable over any backdrop the glass samples.
static const TextStyle _buttonLabel = TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
shadows: [
Shadow(color: Colors.black38, blurRadius: 8, offset: Offset(0, 1)),
],
);
@override
Widget build(BuildContext context) {
final bottomInset = _usesNativeTabBar
? _theme.nativeShellBottomInset(context)
: _theme.contentBottomInset(context);
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text('Liquid Glass Kit'),
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
titleTextStyle: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
shadows: [Shadow(color: Colors.black26, blurRadius: 8)],
),
),
body: DecoratedBox(
// Vivid backdrop with shapes: glass is transparent and only visible
// over detailed content.
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF3A0CA3),
Color(0xFF7209B7),
Color(0xFFB5179E),
Color(0xFF4CC9F0),
],
stops: [0.0, 0.35, 0.65, 1.0],
),
),
child: Stack(
children: [
const _BackdropShapes(),
ListView(
padding: EdgeInsets.fromLTRB(
24,
MediaQuery.paddingOf(context).top + 72,
24,
bottomInset,
),
children: [
Align(
alignment: Alignment.centerLeft,
child: LiquidGlassContainer(
borderRadius: 20,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: LiquidGlassPlatform.supportsNativeGlass
? const Color(0xFF30D158)
: Colors.orangeAccent,
),
),
const SizedBox(width: 8),
Text(
LiquidGlassPlatform.supportsNativeGlass
? 'Native glass · iOS '
'${LiquidGlassPlatform.osMajorVersion} · '
'Tab: ${_tabs[_currentIndex].title}'
: 'Plain fallback · '
'Tab: ${_tabs[_currentIndex].title}',
style: _buttonLabel.copyWith(fontSize: 13),
),
],
),
),
),
const SizedBox(height: 32),
_sectionTitle('Buttons'),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
LiquidGlassButton(
onPressed: () => _showTapped('Regular'),
child: const Text('Regular', style: _buttonLabel),
),
LiquidGlassButton(
style: _clearStyle,
onPressed: () => _showTapped('Clear'),
child: const Text('Clear', style: _buttonLabel),
),
LiquidGlassButton(
style: _tintedStyle,
onPressed: () => _showTapped('Tinted'),
child: const Text('Tinted', style: _buttonLabel),
),
LiquidGlassButton(
style: _darkStyle,
onPressed: () => _showTapped('Dark'),
child: const Text('Dark', style: _buttonLabel),
),
LiquidGlassButton(
style: const LiquidGlassStyle(nativeInteractive: false),
onPressed: () => _showTapped('No shimmer'),
child: const Text('No shimmer', style: _buttonLabel),
),
const LiquidGlassButton(
onPressed: null,
child: Text('Disabled', style: _buttonLabel),
),
],
),
const SizedBox(height: 28),
_sectionTitle('Icon buttons'),
const SizedBox(height: 12),
Wrap(
spacing: 14,
runSpacing: 14,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
LiquidGlassIconButton(
icon: const Icon(Icons.favorite),
iconColor: Colors.white,
onPressed: () => _showTapped('Favorite'),
),
LiquidGlassIconButton(
icon: const Icon(Icons.share),
iconColor: Colors.white,
style: _clearStyle,
onPressed: () => _showTapped('Share'),
),
LiquidGlassIconButton(
icon: const Icon(Icons.add),
iconColor: Colors.white,
style: _tintedStyle,
size: 60,
iconSize: 28,
onPressed: () => _showTapped('Add'),
),
LiquidGlassIconButton(
icon: const Icon(Icons.settings),
iconColor: Colors.white,
style: _darkStyle,
onPressed: () => _showTapped('Settings'),
),
],
),
const SizedBox(height: 28),
_sectionTitle('Icon + label'),
const SizedBox(height: 12),
LiquidGlassButton(
height: 56,
onPressed: () => _showTapped('Continue'),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.arrow_forward, color: Colors.white, size: 20),
SizedBox(width: 8),
Text('Continue', style: _buttonLabel),
],
),
),
if (_usesNativeTabBar) ...[
const SizedBox(height: 28),
_sectionTitle('Tab bar controls'),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_controlButton(
_barVisible ? 'Hide bar' : 'Show bar',
() async {
setState(() => _barVisible = !_barVisible);
await _shell.setVisible(visible: _barVisible);
},
),
_controlButton('Select Search', () async {
await _shell.setSelectedIndex(1);
setState(() => _currentIndex = 1);
}),
_controlButton(
'Clear selection',
() => _shell.clearSelectedIndex(),
),
_controlButton(
_badgeOn ? 'Clear badge' : 'Set badge',
() async {
setState(() => _badgeOn = !_badgeOn);
await _shell.configureTabs(_tabs, theme: _theme);
},
),
_controlButton('Open sheet', () {
_shell.presentModalBottomSheet<void>(
context: context,
backgroundColor: const Color(0xFF2A1458),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(24),
),
),
builder: (context) => Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'The native tab bar hides while this '
'sheet is open and comes back when it '
'closes.',
textAlign: TextAlign.center,
style: _buttonLabel,
),
const SizedBox(height: 20),
LiquidGlassButton(
style: _tintedStyle,
onPressed: () =>
Navigator.of(context).pop(),
child: const Text(
'Close',
style: _buttonLabel,
),
),
const SizedBox(height: 16),
],
),
),
);
}),
],
),
],
],
),
],
),
),
// Floats above the native tab bar via the package's FAB location.
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFF0A84FF),
foregroundColor: Colors.white,
onPressed: () => _showTapped('FAB'),
child: const Icon(Icons.edit),
),
floatingActionButtonLocation: _usesNativeTabBar
? const LiquidGlassNativeTabShellFabLocation()
: FloatingActionButtonLocation.endFloat,
// iOS 26+: Apple's real UITabBarController overlays this scaffold, so
// no Flutter bar is rendered. Elsewhere: a plain Flutter bar.
bottomNavigationBar: _usesNativeTabBar
? null
: LiquidGlassBottomNav(
items: _fallbackNavItems,
currentIndex: _currentIndex,
onIndexChanged: (index) => setState(() => _currentIndex = index),
),
);
}
}
/// Soft glowing orbs plus a couple of crisp accents behind the glass — the
/// material needs detail to refract, but the shapes stay out of the
/// content's way and blend with the gradient instead of fighting it.
class _BackdropShapes extends StatelessWidget {
const _BackdropShapes();
@override
Widget build(BuildContext context) {
Widget orb(double size, Color color, {double opacity = 0.55}) =>
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
color.withValues(alpha: opacity),
color.withValues(alpha: 0),
],
stops: const [0.15, 1.0],
),
),
);
Widget ring(double size, Color color) => Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: color.withValues(alpha: 0.8),
width: 10,
),
),
);
return Positioned.fill(
child: Stack(
children: [
Positioned(top: -60, left: -80, child: orb(320, Colors.white, opacity: 0.7)),
Positioned(top: 260, right: -120, child: orb(320, const Color(0xFF4CC9F0), opacity: 0.9)),
Positioned(top: 560, left: -100, child: orb(300, const Color(0xFFFF8FAB), opacity: 0.7)),
Positioned(top: 820, right: -60, child: orb(260, const Color(0xFFFFD166), opacity: 0.6)),
Positioned(top: 160, right: 60, child: ring(90, Colors.white)),
Positioned(top: 470, left: 40, child: ring(60, const Color(0xFFFFD166))),
Positioned(top: 700, right: 130, child: ring(70, const Color(0xFF80FFDB))),
],
),
);
}
}