dynamic_glass_glmv 0.2.2
dynamic_glass_glmv: ^0.2.2 copied to clipboard
A Flutter package for reusable dynamic glass UI surfaces and adaptive platform bottom navigation.
dynamic_glass_glmv #
Reusable frosted-glass surfaces and a fully adaptive bottom navigation bar for Flutter.
- iOS 26+ → real
UITabBarController/UITabliquid-glass bar viacupertino_native. - Every other platform (Android, iOS < 26, macOS, web, …) →
GlassPillNavBar: a blurred floating pill bar with an animated sliding selection indicator, styled after modern Telegram Android.
Installation #
Add to your pubspec.yaml:
dependencies:
dynamic_glass_glmv: ^0.2.0
Then run:
flutter pub get
Minimum iOS deployment target: 14.0 (set by
cupertino_native_glmv).
Dart SDK:^3.10.4· Flutter:>=3.3.0
Platform behaviour #
| Platform | What you get |
|---|---|
iOS 26+ (useNativeBottomBar: true, default) |
Native UITabBarController → system liquid-glass material |
iOS 26+ (useNativeBottomBar: false) |
GlassPillNavBar (frosted pill) |
| iOS < 26 | GlassPillNavBar (frosted pill) |
| Android | GlassPillNavBar (frosted pill) |
| macOS / Windows / Linux / Web | GlassPillNavBar (frosted pill) |
The scaffold on iOS (all versions) wraps content in a CupertinoPageScaffold and
overlays the bar in a Stack so the blur bleeds to the screen edge. On Android and other
platforms it uses a Scaffold with extendBody: true so the body scrolls behind the
floating pill.
Quick start #
import 'package:dynamic_glass_glmv/dynamic_glass_glmv.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xFF0A84FF)),
home: DynamicGlassNavigationScaffold(
selectedItemColor: const Color(0xFF0A84FF),
items: const [
AdaptiveNavigationDestination(icon: 'house.fill', label: 'Home'),
AdaptiveNavigationDestination(icon: 'bell.fill', label: 'Inbox', badgeCount: 3),
AdaptiveNavigationDestination(icon: 'person.fill', label: 'Profile'),
],
screens: const [HomeScreen(), InboxScreen(), ProfileScreen()],
),
);
}
}
That single widget gives you:
- Animated sliding glass pill on Android / all non-iOS-26 platforms.
- Native liquid-glass
UITabBarControlleron iOS 26. - Badge drawn as
UITabBarItem.badgeValueon iOS 26, or as a red overlay bubble everywhere else.
Icon types #
AdaptiveNavigationDestination.icon accepts several forms:
| Value | iOS 26 native | iOS fallback | Android / other |
|---|---|---|---|
'house.fill' — SF Symbol name (no /) |
✅ native symbol | mapped CupertinoIcon | mapped Material icon |
'assets/icons/logo.png' — asset path (contains /) |
✅ asset image | ImageIcon |
ImageIcon |
AssetImage('assets/...') |
✅ asset image | ImageIcon |
ImageIcon |
Icons.home / CupertinoIcons.* (IconData) |
❌ silently dropped | Icon widget |
Icon widget |
Any Widget |
❌ silently dropped | used as-is | used as-is |
For best cross-platform parity use SF Symbol names — they are mapped to the nearest
CupertinoIcons on older iOS and the nearest Icons on Android automatically.
GlassPillNavBar #
The default non-iOS-26 renderer is GlassPillNavBar — a floating rounded bar with a
gradient sliding pill inspired by Telegram Android's current bottom navigation.
Standalone usage #
GlassPillNavBar(
items: destinations,
selectedIndex: _index,
onTap: (i) => setState(() => _index = i),
)
Customising the style #
Pass a GlassPillNavBarStyle to glassPillStyle on the scaffold (or directly to
GlassPillNavBar):
DynamicGlassNavigationScaffold(
glassPillStyle: const GlassPillNavBarStyle(
blurSigma: 32,
borderRadius: 32,
pillHeightFactor: 0.70,
tapFeedback: GlassPillTapFeedback.ripple,
showLabels: false, // icon-only mode
),
...
)
Key GlassPillNavBarStyle properties
| Property | Default | What it does |
|---|---|---|
blurSigma |
28 |
Backdrop blur strength |
borderRadius |
28 |
Pill corners of the outer bar |
height |
64 |
Bar content height (dp, before safe-area) |
horizontalPadding |
12 |
Side margin so the bar floats |
bottomPadding |
8 |
Gap above the home indicator |
pillHeightFactor |
0.78 |
Selection pill height as a fraction of bar height |
pillHorizontalInset |
8 |
Extra shrink on each side of a slot for the pill |
pillColors |
frosted white gradient | Custom gradient stop colors for the pill |
pillShineEnabled |
true |
Renders a hairline shine at the top of the pill |
showPill |
true |
Set to false for a pill-less icon-highlight style |
showLabels |
true |
false for icon-only mode |
tapFeedback |
none |
none · ripple · scale |
animationDuration |
320ms |
Pill slide speed |
animationCurve |
easeOutCubic |
Pill slide easing |
borderColor |
white hairline | Solid border around the bar (null disables) |
borderGradient |
null |
Gradient border ring; takes precedence over borderColor |
borderWidth |
1 |
Border / gradient-ring thickness |
shadows |
subtle | List<BoxShadow> under the bar |
pillShadows |
glow + inner | List<BoxShadow> under the pill |
Preset examples
// Telegram dark (AMOLED-friendly)
const GlassPillNavBarStyle(
backgroundColor: Color(0xCC1C1C1E),
borderColor: Color(0x33FFFFFF),
pillColors: [Color(0xFF2C2C2E), Color(0xFF1C1C1E)],
pillBorderColor: Color(0x40FFFFFF),
)
// Accent pill (tinted glass pill instead of white)
GlassPillNavBarStyle(
pillColors: [
accentColor.withValues(alpha: 0.25),
accentColor.withValues(alpha: 0.10),
],
pillBorderColor: accentColor.withValues(alpha: 0.35),
)
// Minimal icon-only, no pill
const GlassPillNavBarStyle(
showLabels: false,
showPill: false,
tapFeedback: GlassPillTapFeedback.scale,
)
// Gradient border ring (takes precedence over borderColor)
const GlassPillNavBarStyle(
borderWidth: 1.5,
borderGradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0x80FFFFFF), Color(0x1AFFFFFF)],
),
)
Custom icon / label renderers #
GlassPillNavBar(
items: items,
selectedIndex: _index,
onTap: (i) => setState(() => _index = i),
// Return any widget — Lottie, SVG, custom painter, …
iconBuilder: (context, item, selected, color, size) {
return LottieBuilder.asset(
selected ? item.selectedIcon : item.icon,
width: size, height: size,
);
},
labelBuilder: (context, item, selected, color) {
return Text(
item.label.toUpperCase(),
style: TextStyle(fontSize: 9, letterSpacing: 0.8, color: color),
);
},
)
iOS 26 exclusive features #
These only take effect when useNativeBottomBar: true (the default) and the app runs on
iOS 26+. They are silently ignored on every other platform.
DynamicGlassNavigationScaffold(
// Minimize the bar when the user scrolls down.
minimizeBehavior: TabBarMinimizeBehavior.onScrollDown,
// Split: trailing item rendered as a separate side pill (Apple News style).
split: true,
rightCount: 1, // number of items in the side pill
splitSpacing: 8.0,
items: [
const AdaptiveNavigationDestination(icon: 'house.fill', label: 'Home'),
const AdaptiveNavigationDestination(icon: 'bell.fill', label: 'Inbox', badgeCount: 2),
const AdaptiveNavigationDestination(icon: 'person.fill', label: 'Profile'),
const AdaptiveNavigationDestination(
icon: 'magnifyingglass',
label: 'Search',
isSearch: true, // → UISearchTab on iOS 26
hideLabel: true, // icon-only in the GlassPillNavBar on other platforms
),
],
screens: const [...],
)
| Feature | Property | iOS 26 | Other |
|---|---|---|---|
| Liquid-glass native bar | useNativeBottomBar: true |
✅ | — |
| Scroll-minimize | minimizeBehavior |
✅ | ignored |
| Search tab | isSearch: true |
✅ UISearchTab | ignored |
| Split / side pill | split: true |
✅ | ignored |
| Badge value | badgeCount |
✅ native badge | ✅ bubble overlay |
| Hide label | hideLabel: true |
best-effort | ✅ |
DynamicGlass surface #
A standalone frosted-glass surface you can wrap around any content:
DynamicGlass(
blur: 24,
opacity: 0.18,
tint: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white38),
padding: const EdgeInsets.all(16),
child: const Text('Content behind this is blurred.'),
)
| Property | Default | |
|---|---|---|
blur |
20 |
ImageFilter sigma |
tint |
0xFFFFFFFF |
Color blended over the blurred area |
opacity |
0.16 |
Alpha applied to tint |
borderRadius |
zero |
Clip + decoration radius |
border |
null |
Optional BoxBorder |
padding / margin |
null |
Inner / outer spacing |
Adaptive native controls #
A set of thin wrappers around cupertino_native_glmv
that embed the real platform control on iOS/macOS — picking up the system look,
including the iOS 26 liquid-glass treatment, automatically — and fall back to
a Flutter rendering everywhere else. They are safe to instantiate on any platform.
Each control re-exports its native parameters 1:1 under the Dynamic* namespace,
so you never need to import cupertino_native_glmv directly.
| Widget | Wraps | Notes |
|---|---|---|
DynamicSwitch |
UISwitch / NSSwitch |
On/off toggle. DynamicSwitchController for imperative updates. |
DynamicSlider |
UISlider / NSSlider |
min / max / step, track & thumb colors. DynamicSliderController. |
DynamicSegmentedControl |
UISegmentedControl |
Labels and/or SF Symbols per segment. |
DynamicButton |
UIButton / NSButton |
Text or round .icon variant; DynamicButtonStyle (incl. glass). |
DynamicPopupMenuButton |
native popup button | Text or .icon trigger; DynamicPopupMenuItem / DynamicPopupMenuDivider. |
DynamicIcon |
SF Symbol | Native symbol rendering (monochrome / hierarchical / palette / multicolor). |
DynamicSymbol |
CNSymbol |
SF Symbol descriptor: name, size, color, palette, rendering mode, gradient. |
Disabled semantics #
Mirroring Material's APIs, the value controls treat a null change callback as
disabled:
DynamicSwitch(value: on, onChanged: null) // disabled
DynamicSlider(value: v, onChanged: locked ? null : (x) {…}) // disabled while locked
DynamicSegmentedControl(..., onValueChanged: null) // disabled
You can also pass enabled: false explicitly. A control is disabled when either
enabled is false or its callback is null.
Examples #
// Switch with a tint and a controller.
final switchController = DynamicSwitchController();
DynamicSwitch(
value: _on,
color: const Color(0xFF0A84FF),
controller: switchController,
onChanged: (v) => setState(() => _on = v),
);
// Slider 0–100 in steps of 5.
DynamicSlider(
value: _volume,
min: 0, max: 100, step: 5,
onChanged: (v) => setState(() => _volume = v),
);
// Segmented control with SF Symbols.
DynamicSegmentedControl(
labels: const ['Day', 'Week', 'Month'],
selectedIndex: _segment,
onValueChanged: (i) => setState(() => _segment = i),
);
// Glass text button + round icon button.
DynamicButton(
label: 'Continue',
style: DynamicButtonStyle.glass,
onPressed: _submit,
);
DynamicButton.icon(
icon: const DynamicSymbol('heart.fill'),
tint: const Color(0xFFFF375F),
onPressed: _like,
);
// Popup menu button (icon trigger).
DynamicPopupMenuButton.icon(
buttonIcon: const DynamicSymbol('ellipsis.circle'),
items: const [
DynamicPopupMenuItem(label: 'Share', icon: DynamicSymbol('square.and.arrow.up')),
DynamicPopupMenuDivider(),
DynamicPopupMenuItem(label: 'Delete', icon: DynamicSymbol('trash')),
],
onSelected: (i) => _handleMenu(i),
);
// Native SF Symbol icon.
DynamicIcon(
symbol: const DynamicSymbol('sparkles', size: 28, color: Color(0xFF0A84FF)),
);
iOS-only parameters (e.g. SF Symbol names,
DynamicButtonStyle.glass) degrade gracefully on other platforms — the Flutter fallback renders a sensible equivalent.
AdaptiveBottomNavigationBar (lower-level) #
If you manage the scaffold yourself, drop in the bar directly:
AdaptiveBottomNavigationBar(
items: items,
selectedIndex: _index,
onTap: (i) => setState(() => _index = i),
selectedItemColor: Theme.of(context).colorScheme.primary,
glassPillStyle: const GlassPillNavBarStyle(blurSigma: 40),
// Override everything for the non-iOS-26 branch:
fallbackNavBar: MyCustomBar(...),
)