liquid_glass_widgets 0.29.1
liquid_glass_widgets: ^0.29.1 copied to clipboard
iOS 26-style liquid glass widgets for Flutter. Built on a custom fragment shader for real blur, liquid interactions, specular highlights, and chromatic aberration.
0.29.1 #
Fixes #
GlassSegmentedControl — duplicate unlabeled semantics node (#188)
Each segment emitted two tappable nodes (one unlabeled), breaking VoiceOver/TalkBack. Fixed by adding excludeFromSemantics: true to the internal GestureDetector; semantics are fully handled by GlassFocusRegion.
GlassTabBar shadow lost in Dark OS + Light app (#124)
Shadow disappeared when the device was in Dark Mode but ThemeMode.light was set. Introduced a zero-material IoC bridge: pass brightnessResolver: Theme.maybeBrightnessOf to LiquidGlassWidgets.wrap() so the package correctly honours ThemeMode without importing flutter/material.dart.
Migration — MaterialApp users must add this one line to fix #124:
runApp(LiquidGlassWidgets.wrap( child: const MyApp(), brightnessResolver: Theme.maybeBrightnessOf, ));
CupertinoAppusers: no change required.
Dead flutter/material.dart import removed from tab_bar_bottom_internal.dart
Leftover from pre-0.26.0, never cleaned up. lib/ is now 100% zero-material for cupertino_ui compatibility.
Eliminates production ANRs on Android devices running Impeller GLES (devices without Vulkan support, including many MediaTek and budget Qualcomm Snapdragon SoCs).
Root cause #
On Android GLES, glCompileShader + glLinkProgram executes synchronously on the
Flutter raster thread at first use (100–800 ms on mid-range hardware). When this
coincides with FlutterJNI.nativeSurfaceChanged during surface setup, Android's
watchdog declares an ANR. The previous warm-up implementation instantiated a
LiquidGlassLayer widget outside the widget tree — an unmounted widget is never
rasterized, so no GPU work occurred. The warm-up was a no-op.
Fix #
-
True GPU warm-up (
liquid_glass_setup.dart):_warmUpImpellerPipeline()now draws both premium glass shaders to a 1×1 off-screen surface usingPicture.toImage()and awaits rasterization. This forces GLES pipeline compilation on the raster thread whileinitialize()is still running — beforerunApp— so compilation completes behind the native splash screen and cannot race with surface setup. -
Android-only execution: The warm-up is guarded by
defaultTargetPlatform == TargetPlatform.android. iOS and macOS use precompiled Metal shaders and skip this step entirely, preserving their zero startup overhead. -
Reuses cached programs: The warm-up now calls
MultiShaderBuilder.cachedProgram()to retrieve theFragmentProgramobjects already loaded byprecacheShaders()in step 1 ofinitialize(). No duplicate GPU objects are created. -
warmUpImpellerPipelineparameter:LiquidGlassWidgets.initialize()accepts a newwarmUpImpellerPipeline: boolparameter (defaulttrue). On non-Android platforms the parameter is a no-op. Passfalseonly if you are managing Android shader warm-up yourself. -
Conservative Android quality seeding (
glass_adaptive_scope.dart): Fixed a code-comment mismatch in_GlassAdaptiveScopeState.initState. The file header documented seeding atGlassQuality.standard; the code seeded atmaxQuality(premium). On Android,initStatenow correctly seeds atGlassQuality.standardso Phase 2 benchmarks the device from a stable baseline. iOS and macOS continue to seed atmaxQualityfor an immediate premium experience.
No action required #
Existing call sites (await LiquidGlassWidgets.initialize()) are unchanged and
benefit from the fix automatically. The adaptiveQuality: true path also benefits
from the corrected initState seeding on Android.
Documentation #
- README Platform Support table now distinguishes Android Vulkan from Android GLES and links to a new Android GLES mitigation section.
0.29.0 #
🎵 iOS 26 tabViewBottomAccessory Support #
Added bottomAccessory / bottomAccessoryHeight / bottomAccessoryEnabled / bottomAccessorySpacing / bottomAccessoryPlacement to both GlassTabBar.bottom and GlassTabBar.searchable — mirroring Apple's tabViewBottomAccessory modifier.
- Expanded mode — accessory floats directly above the nav bar pill with a configurable spacing gap (default
6.0px, calibrated to match Apple's native spacing). - Inline mode (
searchableonly) — setbottomAccessoryPlacement: GlassTabBarAccessoryPlacement.inlineto have the accessory slide horizontally into the gap between the collapsed tab indicator and search capsule, with a simultaneous width squish, matching the iOS 26.inlineplacement. - Two independent animation timelines —
accessoryTdrives the inline↔expanded morph (height, left, right) whilesearchTtracks the tab-pill→search-capsule height change so the accessory follows the bar downward during the search activation, maintaining a consistent visual overlap gap throughout. - Safe defaults —
bottomAccessoryPlacementdefaults to.expanded. The accessory never collapses inline automatically; developers must explicitly opt into.inlineplacement, mirroring the iOS 26 model where placement intent is declared at the call site. - Pixel-accurate scaffold insets —
preferredSizeis always in sync with the layout engine soGlassScaffold's edge fade reserves the exact correct amount of space in both expanded and inline states. GlassTabBarAccessoryPlacementenum —expandedandinlinevalues, readable inside the accessory widget itself viaGlassTabBarAccessoryPlacementScope.of(context)to adapt the accessory's own layout between the full row and compact strip.- Apple Music and Apple Podcasts demos fully showcase the feature — including the expanded/inline transition and the search-active behaviour.
Upgrading from bodyOverlays #
Previously, the recommended pattern for a floating mini-player was to place it in GlassScaffold.bodyOverlays and manually manage its position using AnimatedPositioned with scroll-offset math. That approach still works and bodyOverlays remains available for other use cases (e.g. floating action overlays, toast banners).
For a bottom accessory that is architecturally part of the tab bar — which is exactly what iOS 26 tabViewBottomAccessory models — the bottomAccessory API is the correct replacement:
// Before (bodyOverlays workaround)
GlassScaffold(
bodyOverlays: [
AnimatedPositioned(
bottom: _isMiniMode ? barH : barH + accessoryH + spacing,
left: 0, right: 0,
child: MiniPlayer(),
),
],
)
// After (iOS 26-aligned)
GlassTabBar.searchable(
bottomAccessory: MiniPlayer(),
bottomAccessoryHeight: 50.0,
bottomAccessoryPlacement: _isMiniMode && !_isSearching
? GlassTabBarAccessoryPlacement.inline
: GlassTabBarAccessoryPlacement.expanded,
)
The new API removes all manual position arithmetic, keeps the GlassScaffold edge fade pixel-accurate, and persists the accessory automatically across tab switches.
0.28.1 #
📚 Internal Refactor — API Documentation #
- 100% Dartdoc coverage — every public member is documented.
public_member_api_docsis now permanently enabled inanalysis_options.yaml; future undocumented public API additions will faildart analyze. - Internal layout engines moved to
lib/src/— 9 internal implementation files relocated per Dart convention sodart docand pub.dev omit them from the generated API reference. No public API changes. 2517 tests passing.
0.28.0 #
🌐 Full RTL (Right-to-Left) Support #
Completed the Right-to-Left (RTL) layout audit. All directional padding and
alignment primitives now use EdgeInsetsDirectional / AlignmentDirectional
so widgets mirror correctly in RTL locales (Arabic, Hebrew, Persian, etc.)
without any API changes for existing callers.
Widgets Updated #
GlassGroupedSection— header and footer labels now useEdgeInsetsDirectional.only(start: 16, end: 16). Under RTL the text aligns to the correct physical edge.GlassDivider— horizontal dividerindent/endIndentnow useEdgeInsetsDirectional.only(start: indent, end: endIndent). The leading indent is always on the logical leading side regardless of text direction. Vertical dividertop/bottomare unchanged (not directional).GlassAppBar— non-centered title now usesAlignmentDirectional.centerStart+EdgeInsetsDirectional.only(start: 8)so the title anchors to the leading edge in both LTR and RTL.GlassSearchBar— cancel button gap usesEdgeInsetsDirectional.only(start: 10)so the gap appears between the search field and the cancel button in both directions.GlassDialog— horizontal two-button layout gap usesEdgeInsetsDirectional.only(start: 8)so buttons remain correctly spaced in RTL.GlassLargeTitle— two improvements:paddingandsearchBarPaddingdefaults changed fromEdgeInsets.*toEdgeInsetsDirectional.*— the field types were alreadyEdgeInsetsGeometry, so this is a zero-breaking-change improvement. Callers who pass custom asymmetric directional padding (e.g.EdgeInsetsDirectional.only(start: 32)) now get correct physical mirroring in RTL.Transform.scalealignment changed fromAlignment.bottomLefttoAlignmentDirectional.bottomStart— under RTL, the rubber-band overscroll stretch now scales from the correct logical leading edge rather than always pinning to the physical left.
GlassProgressIndicator.linear— custom canvas drawing now explicitly respectsDirectionality. In RTL locales, both the determinate fill and indeterminate moving bar correctly animate from the physical right (logical start) to the left.GlassSlider— drag logic and active track drawing now invert cleanly under RTL. Dragging left increases the value, and the track anchors to the physical right.
Bug Fixes & Accessibility #
GlassTabBar.bottom— Resolved an edge-case visual bug in RTL mode where the animated glass pill would jump to the mirror-image tab when selecting an end tab. Fixed by enforcingAlignment(x, y)physical coordinates instead ofAlignmentDirectionalwithin the physics engine bounds.GlassSegmentedControl&GlassTabBar— Unselected segments now correctly emit a "not selected" accessibility state (resolves #184). Thanks to @Xodus-CO for the detailed report!
0.27.0 #
♿ Accessibility & Keyboard Navigation #
Every interactive widget now supports full keyboard traversal, Space/Enter activation, and VoiceOver/TalkBack semantics.
Focus & Keyboard #
GlassFocusRegion— new shared widget that is the single source of truth for all focus behaviour. Two modes:- Interactive — wraps
FocusableActionDetector, registersActivateIntent(Space/Enter), lifts hover/focus state to parent viaValueNotifier, and paints the iOS 26-style outset focus ring. - Observe (
.observe()) — for widgets that own their ownFocusNode(e.g.GlassTextField). Listens passively and paints the ring; no duplicate traversal or intent handling.
- Interactive — wraps
- iOS 26 focus ring — 3 px outset, 2 px wide, rounded to the widget's shape. Implemented as
GlassFocusRingPainter(pureCustomPainter; zero cost for touch users viaValueListenableBuilder). GlassStepperkeyboard fix — Space/Enter triggers a single-shot activation; the pointer-only long-press repeat timer is no longer erroneously started by keyboard input.- All 12 interactive widget families wired:
GlassButton,GlassSwitch,GlassSlider,GlassSegmentedControl,GlassListTile,GlassMenuItem,GlassActionSheet,GlassButtonGroup,GlassTextField,GlassSearchBar,GlassStepper,GlassChip.
Semantics #
- Semantic roles (
isButton,isSlider,isSelected,toggled,value) set on every widget matching Material and Cupertino SDK conventions. - Destructive and disabled states propagate correctly to the semantic tree.
GlassBadge—semanticLabelandsemanticCountoverrides — resolves two user-reported limitations:semanticLabel(optionalString?) fully replaces the VoiceOver/TalkBack announcement; enables non-notification domains ("5 downloads", "Online", etc.).semanticCount(optionalint?) overrides only the spoken number while leaving the visual cap (99+) unchanged; allows "2500 notifications" to be announced when the badge visually shows "99+".- Both constructors (
GlassBadgeandGlassBadge.dot) supportsemanticLabel;semanticCountis only applicable to count badges. - Fully backwards-compatible — existing callers with no overrides get identical defaults.
GlassPageControl—semanticLabelparam — the capsule now announces'Page N of M'to VoiceOver/TalkBack by default (1-indexed). AsemanticLabeloverride lets callers substitute domain-specific wording (e.g.'Slide 3 of 5'). Non-interactive controls (noonPageChanged) omit the tap hint automatically.GlassPasswordField— toggle button semantics — the show/hide password suffix icon is now wrapped inSemantics(label: 'Show password' / 'Hide password', button: true). Previously theGestureDetectorwas invisible to screen readers; VoiceOver now correctly announces the button and its toggled state.GlassProgressIndicator—semanticLabelparam — the hardcoded'Progress'label is now an overridable default. Callers can passsemanticLabel: 'Download progress'(or any domain string) to both.circular()and.linear()constructors. Backwards-compatible.GlassPullDownButton—semanticLabelparam — icon-only pull-down buttons previously announced an empty string. A newsemanticLabelparam (e.g.'More options') is used as theGlassButton.labelin the icon-only code path. Visible-label variants are unaffected.
Layout & Architecture #
GlassInteractionStateMixin— internalStatemixin (modelled onSingleTickerProviderStateMixin) providingisPressed,isFocused,isHoveredValueNotifiers and theirListenable.mergecombinations. Replaces ~76 lines of identical boilerplate acrossGlassListTile,GlassMenuItem,_ActionSheetButton, and_GlassGroupItemWidget. No public API change.- All container-item widgets migrated to
ValueNotifier+ListenableBuilder— only theAnimatedContainerhighlight layer rebuilds on interaction; the surrounding subtree is stable.
0.26.1 #
🐛 Bug Fixes #
GlassAdaptiveScopequality oscillation afterreset()— whenallowStepUp: false, areset()-triggered warm-up could silently promote quality back up (e.g.standard → premium). Warm-up now respects the flag and can only confirm or lower the current quality, never raise it. Thanks to @jingluoguo for the contribution (#180).
0.26.0 #
💥 Breaking Changes #
-
GlassScaffold.floatingActionButtonremoved — iOS does not use Floating Action Buttons.floatingActionButtonis removed entirely; this package is pre-v1 and that is the window to get the API right.Migration:
// Before GlassScaffold( floatingActionButton: FloatingActionButton(onPressed: _add, child: Icon(Icons.add)), body: ..., ) // After — glass-treated, iOS-idiomatic GlassScaffold( bodyOverlays: [ Positioned( bottom: 24, right: 24, child: GlassButton( onTap: _add, child: const Icon(CupertinoIcons.add, color: CupertinoColors.white), ), ), ], body: ..., )bodyOverlaysis above the body and below the bars.GlassButtonapplies the correct liquid glass treatment.
♻️ Refactoring — Material Decoupling (material.dart 36 → 0) #
GlassScaffold now uses CupertinoPageScaffold internally. GlassPage no
longer injects a Material Theme shim. All Colors.* constants replaced with
CupertinoColors or explicit hex literals. No visual changes.
glass_brightness.dart was completely rewritten to drop its dependency on
Theme.maybeBrightnessOf. It now reads CupertinoTheme.of(context).brightness,
which natively inherits from the Material ThemeMode when used inside a
MaterialApp (thanks to Flutter's automatic MaterialBasedCupertinoThemeData
injection).
With this final swap, liquid_glass_widgets now has zero imports of
package:flutter/material.dart in its lib/ directory. It is fully
decoupled and ready for the cupertino_ui package split. The example's
Apple replica demos (apple_music, apple_podcasts, apple_messages,
apple_news, apple_lockscreen) run with zero Material imports as well,
proving the library works purely within a Cupertino-only context.
🐛 Bug Fixes #
LightweightLiquidGlassincorrect brightness inGlassThemeoverride contexts — was callingTheme.of(context).brightnessinstead of the canonicalGlassTheme.brightnessOf(context). Could produce wrong brightness whenGlassThemeset a different mode than the ambient Material theme. Fixed.
0.25.1 #
🐛 Bug Fixes #
GlassMenumispositioned inside nestedNavigators —OverlayPortalnow targets the rootOverlay(viaOverlayChildLocation.rootOverlay), matching the screen-global coordinates captured bylocalToGlobal. Previously the menu drifted by the offset of the nearest nestedOverlayfrom the screen edge (e.g. a side-rail in aStatefulShellRoutelayout). Thanks to @sinanhaci for the contribution (#179).
0.25.0 #
✨ New Features #
-
Optional sheet detents —
GlassModalSheet(and.show()) gained adetentsset (Set<GlassSheetDetent>, mirroring UIKit's sheet detents) plus adismissibleflag, to compose which stops a sheet offers:{GlassSheetDetent.medium}→ a half-only glass sheet that never morphs to the opaque full state (its content still scrolls at the half detent).{GlassSheetDetent.large}→ a full-only opaque sheet that opens straight to full.{GlassSheetDetent.medium, GlassSheetDetent.large}→ the default two-stop sheet.{GlassSheetDetent.small, ...}→ adds the maps-style peek floor underneath, so one mechanism now describes every resting stop.dismissible: false→ the sheet rubber-bands at its lowest detent instead of swiping away (the Apple Pay / Sign in with Apple pattern).
The set must be non-empty (asserted). Style the small detent with the existing
peek*params (peekSettings,peekWidth, …), which stay top-level: aSetwhose members carried per-instance payload would need equality that ignores that payload, ordetents.contains(small)breaks and two differently-configured smalls could sit in one set.
⚠️ Deprecations #
GlassModalSheet.enablePeek→ useGlassSheetDetent.smallindetents. Peek is now a detent like medium and large.enablePeekis still honoured and takes precedence over the set when set explicitly, so existing code keeps working unchanged; it will be removed in a future release.GlassSheetMode.persistentkeeps its peek floor with or without the detent — a persistent sheet is defined by resting rather than dismissing.
🐛 Bug Fixes #
GlassModalSheetControllerreattachment — the controller no longer detaches when itsGlassModalSheetis swapped under a stable controller (e.g. aValueKeychange): the replacement'sinitStateruns before the outgoing widget'sdispose, so the detach is now guarded to only clear the attachment it still owns. Previously this left the controller inert and the sheet un-openable.- Half-only content scrolling — a sheet's inner scroll view now enables at
the sheet's topmost detent rather than hardcoding the
fullstate, so a half-only sheet scrolls its content correctly.
Thanks to @jfhair for the contribution (#178).
0.24.4 #
🐛 Bug Fixes #
GlassBottomBarcollapse trajectory fixed — whenGlassBottomBarCollapseConfigwithdirection: towardsExtraButtonwas set, the collapsed tab pill stopped short of the extra button, leaving a visible gap between the two circles. The pill now travels to the exact horizontal centre of the extra button on both left and right placements, and the extra button correctly overlays the pill at the end of the animation. Thanks to @jingluoguo for the contribution (#176).
✨ Improvements #
-
GlassBottomBarCollapseConfigdefaultanimationDurationbumped from 220 ms → 280 ms — the collapse animation carries semantic meaning (the tab pill merges into the action button) and the extra 60 ms is enough for the eye to track the trajectory without feeling sluggish. Override it any time via the config:collapseConfig: GlassBottomBarCollapseConfig( animationDuration: Duration(milliseconds: 220), // snappier ),
0.24.3 #
🐛 Bug Fixes #
- Transparent scaffold during navigation fixed —
GlassScaffoldwas unconditionally setting the innerScaffold.backgroundColortoColors.transparent, even when nobackgroundwidget was provided. This caused the previous route to bleed through the incoming screen duringMaterialPageRouteslide transitions, making the new page appear transparent. Fixed by only forcing the scaffold transparent whenGlassScaffoldactually has a background widget orbackgroundColorto render — matching the existingGlassPagebehaviour. Screens without an explicit background now inherit the theme's opaquescaffoldBackgroundColor, producing correct, opaque transitions (issue #177).
0.24.2 #
🐛 Bug Fixes #
- Android cold-launch crash fixed — On Android, Flutter's warm-up frame can produce zero-width or unbounded (
Infinity) layout constraints before the window fully resolves.RenderLiquidGlassGeometry._buildGeometryPicturewould attempt to scale andceil()those non-finite bounds, throwingUnsupportedError: Infinity or NaN toInt. Fixed by guarding against empty or non-finite bounds and returning a safe emptyPicturefor the one affected frame. A matching guard was added inLiquidGlassRenderObject.paintto prevent the same non-finite values propagating intotoImageSync. - Compositing bits race condition fixed (Vulkan/Impeller) — When
LightweightLiquidGlassorLiquidGlassRenderObjectchanged settings that affectalwaysNeedsCompositing, the compositing bit was not synchronised, causing incorrect layer decisions on the first paint under Vulkan. Fixed by callingmarkNeedsCompositingBitsUpdate()whenever the compositing contract changes.
0.24.1 #
🐛 Bug Fixes #
GlassButton.customborder invisible in release builds with--obfuscate—GlassEffectandLightweightLiquidGlasswere usingdynamicproperty access andruntimeType.toString()heuristics to extract the shape's corner radius for the shader. Both techniques are silently broken by the Dart AOT obfuscator: property names are mangled and class names become single-character tokens. The fallback silently resolved to0.0, producing a perfectly square (borderless) shape in release builds. Fixed by adding a typedeffectiveRadiusabstract getter toLiquidShapeimplemented by every concrete shape class — a single virtual dispatch that the compiler can optimise and the obfuscator cannot break.
0.24.0 #
⚠️ Breaking Changes #
-
GlassAppBar.preferredSizeconstructor parameter removed. Replace withtoolbarHeight: double(default44.0).// Before GlassAppBar(preferredSize: Size.fromHeight(52)) // After GlassAppBar(toolbarHeight: 52)
🐛 Bug Fixes #
GlassScaffolddark-mode gradient flash fixed. WhenbackgroundColoris set and the device is in dark mode, the edge-fade gradient aroundGlassTabBar/GlassBottomBarcould briefly flash dark. The scaffold now always renders with a transparent Material background (preventing theme bleed), and passes the explicitbackgroundColorto the edge-effect fallback gradient.GlassBottomBardefault radius inconsistency fixed. Was32.0; nowGlassDefaults.capsuleRadius— consistent withGlassTabBar.bottom.- Indicator radius inconsistent across widgets. All interactive widgets (
GlassSegmentedControl,GlassTabBar,GlassBottomBar) now share a unified radius calculation that is guaranteed correct during jelly-bloom expansion.
✨ New Features #
-
3-tier indicator radius system — all interactive bar/control widgets now follow iOS 26 defaults out of the box:
- Tier 1 (default): bar and indicator are both a perfect capsule — no configuration needed.
- Tier 2: set
barBorderRadiusto a custom value and the indicator automatically tracks atbarBorderRadius − padding(concentric nested arcs). - Tier 3: set
indicatorBorderRadiusexplicitly to override everything.
-
GlassDefaults.capsuleRadius— named constant (9999.0) for the capsule sentinel:GlassBottomBar(barBorderRadius: GlassDefaults.capsuleRadius) -
GlassAppBar.bottom— Accepts anyPreferredSizeWidget(typically aTabBar) rendered below the navigation bar title. The scaffold automatically reserves the combined height — no manual sizing needed.GlassAppBar( title: const Text('Browse'), bottom: TabBar(tabs: [...]), )
0.23.0 #
⚠️ Breaking Changes #
-
AnimatedGlassIndicator.useSuperellipseremoved. This parameter has been deleted from the constructor. Any call site passinguseSuperellipse: trueoruseSuperellipse: falsewill fail to compile.Migration: simply remove the parameter. The indicator is always a rounded rectangle (capsule) now, which is mathematically correct. Squircle geometry is unstable for dynamic stretching elements.
// Before (0.22.1) AnimatedGlassIndicator( useSuperellipse: false, ... ) // After (0.23.0) — just remove the parameter AnimatedGlassIndicator( ... )
✨ New Features #
-
LiquidGlassSettings.fresnelStrength— New parameter (range0.0–1.0, default1.0) that scales the natural Fresnel edge luminosity on the Premium rendering path. At1.0the glass behaves as physically lit glass with a rim highlight at grazing angles (existing default). At0.0the rim is completely suppressed, producing a pure blur-overlay appearance that matches iOS 26 system UI glass (Messages buttons, notification banners, lock screen controls). Intermediate values interpolate smoothly. Fully backwards compatible — omitting the parameter preserves all existing rendering. Also exposed onGlassThemeSettingsso it can be set app-wide viaGlassTheme. -
GlassMenuItem.enablePressScale— Newboolparameter (defaulttrue) that controls the 0.98× scale-down animation on press. Set tofalseon fill-rate-limited devices to eliminate the per-frame GPU cost of animating aTransform.scaleover the glass layer. Fully backwards compatible. -
GlassExtraButtonPlacement— AddedGlassExtraButtonPlacement.leftandrightfor non-searchable bottom bars soGlassTabBarExtraButtoncan be placed on either side. Defaults torightto preserve existing behavior. Thanks to @jingluoguo (#169).
🐛 Bug Fixes #
- Fixed hard clip at the top of
useOwnLayer: truebuttons during press-scale animation on Impeller. - GlassSlider — Fixed an issue where discrete slider snapping would round the absolute value and shift the snapped range when using a non-zero minimum. Thanks to @huanglizhuo (#168).
♻️ Refactoring — Pure Geometry & iOS 26 Shape Parity #
This release fundamentally solves the long-standing geometry tension between Flutter's path rendering and our GPU shaders. We completely rewrote the squircle math to use pure analytical curves, fixed stretching bugs in tab indicators, and simplified the API.
1. Pure Analytic Lamé Squircles #
- Shader Rewrite (
sdf.glsl): We completely removed the old piecewise 45-degree seam approach and the hackyblendsafety valve.sdfSquirclenow uses a pure, analytic Lamé curve (|x|^n + |y|^n = 1). Squircles now perfectly match Apple's continuous curve geometry with zero flattening on the edges. - Graceful Degradation (The Ghost-Glow Fix): When a squircle is given a radius that physically cannot fit (e.g.
r = 18on a36pxtall button), the shader now dynamically recalculates the exponentnbased on the clamped available space. As space runs out,nsmoothly degrades to2.0, collapsing into a perfect circle. This mathematically guarantees that the shader's interaction glow always aligns perfectly flush against Flutter's clipping path, eliminating the dark corner gaps.
2. Perfect iOS 26 Pills (Capsules) #
Apple never uses squircles for pill shapes (like "Edit" buttons or Tab Indicators). They use pure circular-arc capsules. We audited the library to align with this:
GlassChip& Demo Buttons: ReplacedLiquidRoundedSuperellipsewithLiquidRoundedRectanglefor all pill-shaped elements. They now explicitly usesdfRRectAsym, ensuring perfect circular ends.GlassMenu&GlassPopovermorph blobs: ReplacedLiquidOvalwithLiquidRoundedRectangle. The rounded rect SDF is mathematically stable at all aspect ratios during dynamic morphs.
3. AnimatedGlassIndicator API Cleanup #
The glass tab indicator previously suffered from a "stretching bug" where it turned squarish during drag expansion because its finite borderRadius was outgrown by its expanding height.
- Removed
useSuperellipse(see Breaking Changes above): This parameter was mathematically incorrect for dynamic stretching indicators. - Optional
borderRadius(Default9999.0):borderRadiusis no longer required. It defaults to9999.0, which offloads the math entirely to the shader'smin(r, shortest)clamp. This guarantees a perfect capsule at any drag size. - Segmented Controls:
GlassSegmentedControlexplicitly passesborderRadius: containerRadius - 3, ensuring it retains its correct inset rounded-rectangle geometry rather than defaulting to a capsule.
📚 Documentation #
shape_debug_demo.dart— corrected the Standard-mode description banner from the inaccurate"_SquircleClipper + lightweight shader (CPU L4/L2 path)"to the accurate"ShapeBorderClipper + lightweight blur shader (shape-blind)". There is no_SquircleClipperclass; the lightweight shader is shape-type-blind by design.
0.22.1 #
🐛 Bug Fixes #
- Tab Bar Semantics — Fixed multiple accessibility issues in
GlassTabBarandGlassBottomBar:GlassTab.semanticLabelis now properly propagated, allowing icon-only tabs to be correctly announced instead of defaulting to'Tab'.- Fixed an issue where tab labels were announced twice by wrapping the internal
Textwidget inExcludeSemantics. - Eliminated duplicate semantic nodes for the active tab by hiding the visual clipping indicator from the accessibility tree, ensuring exactly one node per tab. Thanks to @simiwe (#159).
♻️ Refactoring #
- Internal Tab Models — Deprecated
GlassBottomBarTabis no longer used by the internal layout engines. They now natively acceptGlassTab, removing the need for mapping closures andSizedBox.shrink()sentinels.
0.22.0 #
✨ New Features #
-
ProgressiveBlur— a graduated backdrop blur that is strongest at one edge and dissolves to sharp at the opposite edge (the iOS 26 / Signal header look). Self-contained — noLiquidGlassLayerancestor required.Positioned( top: 0, left: 0, right: 0, height: 96, child: ProgressiveBlur(maxSigma: 20), )maxSigma— blur sigma at the strong edge (0⇒ passthrough).direction— which edge is strongest (topToBottom/bottomToTop/leftToRight/rightToLeft).falloff— gradient gamma (default1.2).
Pre-warmed by
LiquidGlassWidgets.initialize()at no extra startup cost;ProgressiveBlur.preload()is available for standalone use. Degrades to a uniformBackdropFilteron Skia / web. Seedocs/PROGRESSIVE_BLUR.md. Thanks to @Ahmadre (#162).
⚡ Performance #
-
GlassPopoverblur ramp — the backdrop blur now eases in over the opening morph instead of rendering at full strength from frame one. Raster avg halved (10.1 ms → 5.8 ms), worst-case halved, missed-budget frames 15 → 6 on the reference device. Seedocs/POPOVER_BLUR_RAMP.md.Two new backwards-compatible params:
blurRampDuration(defaultDuration(milliseconds: 260)) — set toDuration.zeroto restore the previous always-full-blur behaviour.blurRampCurve(defaultCurves.easeOut).
Automatically disabled when "reduce motion" is active. Thanks to @Ahmadre (#161).
🐛 Bug Fixes #
-
GlassPopoverdrifted off its trigger in nested overlays — the morph portal now targetsOverlayChildLocation.rootOverlayto match the root-relative coordinates it is placed at. Top-level usage is unaffected. Thanks to @Ahmadre (#163). -
Intrinsic-height
GlassPopoveroverflowed on live content growth — the popover now re-measures viaSizeChangedLayoutNotifierwhen content grows while open, instead of clamping to the height frozen at open time. Fixed-popoverHeightpopovers are unchanged. Thanks to @Ahmadre (#163).
0.21.6 #
🐛 Bug Fixes #
GlassTabBar.bottom/GlassBottomBardistorted tap hit regions with >2 tabs (#157) — Tapping a tab incorrectly routed throughDraggableIndicatorPhysics.getAlignmentFromGlobalPosition, which applies an indicator-center remap (±½ tab-widthpadding) designed for continuous drag tracking. For discrete taps this shifted the hit boundaries from the correct25/50/75 %to~31.25/50/68.75 %on a 4-tab bar, making the right ~25 % of tab 2 select tab 3 instead. A newtabIndexFromGlobalPositionhelper bypasses the drag remap and computes the tab index directly from the raw position fraction. The same incorrect remap was present in therecoverIfGestureStuckpath (PlatformView gesture recovery) and is fixed there too. The drag path (onHorizontalDragUpdate/onHorizontalDragStart) is unaffected. Thanks to @bayraktarmdkaraca for the precise root-cause analysis!
0.21.5 #
🐛 Bug Fixes #
GlassTabBarloses shadow when OS is Dark but app isThemeMode.light— fixed an issue where glass components incorrectly resolved toBrightness.darkwhen the device OS was in Dark Mode but theMaterialAppwas explicitly set toThemeMode.light. The brightness cascade inresolveGlassBrightnesswas checking theCupertinoThemebefore the MaterialThemeMode; inside aMaterialAppthe Cupertino theme is implicitly derived from the OS, causing it to return the wrong brightness. The cascade now correctly prioritisesThemeModeinMaterialAppcontexts. Thanks to @minhtritc97 for the detailed report!
0.21.4 #
✨ New Features #
- Vertical
GlassSegmentedControl— fixed controls now acceptdirection: Axis.verticaland an optionalsegmentExtent. Layout, fractional indicator positioning, jelly expansion, drag velocity, snapping, and gesture recognition all follow the vertical axis. The horizontal default and scrollable constructor remain unchanged. Thanks to @F1orian!
🐛 Bug Fixes #
- Impeller shadow corruption fixed —
shadowElevationonLiquidGlassandGlassButtonrendered as solid black circles on Windows, Web, and certain Android emulators due to two known Flutter engine bugs (saveLayertexture corruption on Vulkan andPath.combineclipping failures). The package now selectively bypasses the GPU shadow cutout on affected platforms and uses a safeevenOddwinding rule for the fallback, producing perfect shadows without engine issues.
🧪 Example #
- Added an icon-only vertical segmented control to the Interactive page for tap, drag, indicator, and accessibility testing on a physical device.
0.21.3 #
🐛 Bug Fixes #
- SVG and custom icons restored —
SizedBox-wrapped icons (e.g.SvgPicture) were silently stripped from the render tree since0.20.0. TheSizedBox.shrink()sentinel detection now checkswidth,height, andchildfields so a caller-suppliedSizedBoxwrapping a real icon is always rendered correctly. - Searchable bar pill stays active while dragging — the glass indicator on
GlassTabBar.searchablecollapsed back to its resting state when the finger passed over the currently selected tab mid-drag. The thickness spring now includes thetabIsDraggingguard, matching the behaviour already present inGlassTabBar.bottom. JellyClipperImpeller radius guard — the clip radius is now clamped to strictly less than half the indicator's shortest side, preventing a malformedRRectpath that caused content to vanish under Impeller's Metal renderer in certain animation frames.
🧹 Example #
- Indicator Parity demo calibrated — default refraction set to
1.15(GlassDefaults.refractiveIndex) to match all Apple demos; bothGlassTabBar.inlinevariants now wire live tuner sliders for expansion and pinch strength.
0.21.2 #
🐛 Bug Fixes #
- Extra button stretch disabled over platform views —
GlassTabBarExtraButtonnow correctly disables its stretch effect whenplatformViewBackdrop: true, matching the behavior ofGlassTabBar.bottomand fixing the jittery spring animation. GlassMenuitem scroll wiggle fixed — menu items with wrapped text (e.g.maxLines: 2) could drift sub-pixel vertically during slide-to-select dragging becauseClampingScrollPhysicsallowed fractional scroll offsets even when no overflow was intended. Non-scrollable menus now useNeverScrollableScrollPhysics, locking the content completely in place during drag.GlassPopoverfirst-frame height fixed — the popover was briefly rendered at full-screen height before its content height was known, producing a visible flash and forcing users to wrap content in aSingleChildScrollViewas a workaround. An invisibleOffstagemeasurement pass now runs on Frame 1, letting Flutter's layout engine calculate the exact intrinsic content height before the morph animation starts. The animation launches on Frame 2 with perfect geometry — no mid-flight height correction, no flash, no heuristics.
⚡ Performance #
- Removed unnecessary compositing layer in
GlassBottomBar— theRepaintBoundarywrapping the icon layer in_buildHighQualityModeis now only mounted whenplatformViewBackdrop: true, where it is required for the Platform View capture path (bug #99). In the common case (platformViewBackdrop: false) the boundary was creating a GPU offscreen texture every frame with no caching benefit, since theJellyClipperchanges on every animation frame. GlassPopoveridle trigger optimised — when the popover is fully closed, the trigger widget now skips unnecessaryTransform,Opacity, andIgnorePointercompositing layers. This removes redundant GPU work in the common idle state.
🧹 Code Quality #
- Null-safe trigger child access — replaced a
child!force-unwrap inGlassPopoverInternalwith a null-safechild ?? const SizedBox.shrink()fallback, preventing a hard crash ifchildis ever omitted in a future refactor.
🧪 Example #
- Refraction slider added to Indicator Parity demo tuner — a "Refraction (n)" slider (range 1.0–2.0, default 1.59 matching
GlassTabBar.bottom's internal default) for tuning therefractiveIndexon the Premium (Impeller) glass indicator. Standard indicators do not perform background capture so the parameter has no visual effect there.
0.21.1 #
🐛 Bug Fixes — Standard indicator parity #
- Two-pills misalignment fixed — at
GlassQuality.standard, the background rim and glass lens now share the same shape geometry (ShapeDecoration) and ride the same jellyTransform, eliminating the visible separation mid-morph. Regression reported againstGlassTabBar.inline/ segmented controls. - Indicator collapse on drag fixed — the glass indicator no longer morphs back to the resting pill when dragging over the selected tab. The thickness gate now includes
tabIsDraggingso the indicator stays fully active for the entire gesture duration. - Standard rim thickness normalised — the indicator rim on
GlassQuality.standardis now proportionally mapped fromindicatorSettings.thickness(same value as Premium) rather than scaling from the raw glass-depth value, which produced a ~2.8 px border. The result is a fine hairline that gracefully matches the Premium look on Standard-quality devices.
0.21.0 #
✨ New Features #
GlassModalSheetdrag progress — controller now exposes aprogressgetter andprogressListenablereporting the live 0–1 drag position between half↔full snap points, so hosts can drive coordinated UI in real time. (#148, @jfhair)GlassTabBar.inlinespring control — the.inlinefactory now acceptsspringDescription, matching the other factory constructors. Previously inline tab bars were locked to the shared default spring. (#149, @jfhair)LiquidGlassSettings.ambientRim— tunable full-perimeter rim on the moving indicator pill. Defaults match Apple Music's segmented control (brighter in light mode, off in dark). (#150, @jfhair)AnimatedGlassIndicatorshadow —shadowElevation/shadowinindicatorSettingsnow correctly paints a drop shadow on the glass jelly. Previously these values were silently ignored. (#151, @jfhair)
🐛 Bug Fixes — PlatformView gesture stability #
- Tab bar freeze fixed: Intermittent freeze where the tab indicator stopped
responding after interacting over an iOS
PlatformView(e.g. a map or WebView). The iOS gesture arena can silently drop terminal callbacks, leaving the recognizer wedged. Fixed with proactive cleanup onPointerDown, post-frame recovery, and a gesture ID guard to prevent rapid-tap state corruption. - Hybrid gesture mode on
platformViewBackdrop: true: When the tab bar floats over aPlatformView, the visual indicator now animates to its new position instantly on touch-down (matching native iOS responsiveness), while the actual tab content swap is deferred safely to touch-up. This prevents the iOS UIKit view system from dropping the touch stream mid-gesture due to a mid-frame unmount of thePlatformView. No impact on any screen whereplatformViewBackdropisfalse— those continue to swap instantly on down. - Stretch disabled on
platformViewBackdrop: true: Flutter'sBackdropFiltermust re-acquire the native pixel buffer every time its bounding box changes. Stretch animations resize the glass container, causing a one-frame flicker over aPlatformView. Stretch is now skipped on all bar elements whenplatformViewBackdropis set; press-scale (interactionScale) is unaffected because it is a GPU-level transform that leaves the backdrop bounds stable. No impact on any other screen or platform.
0.20.1 #
🐛 Bug Fix — GlassButton.custom layout expansion #
Fixes: GlassButton.custom unexpectedly expanding to fill bounded parent constraints (e.g., inside AppBar actions) (#146).
GlassButton.customheightnow correctly defaults tonull(matching documented behavior).- The button now properly shrink-wraps to its content when explicit width/height are not provided.
- Migration: No breaking changes. If you were relying on the undocumented
56pxdefault height, explicitly setheight: 56.
0.20.0 #
💥 Breaking — GlassListTile divider refactor #
GlassListTile no longer draws its own divider. The isLast, showDivider,
and dividerIndent parameters have been removed.
Rationale: A list tile should be a clean, position-agnostic item. Divider rendering is a layout concern that belongs to the parent container.
Migration #
Inside GlassGroupedSection — no changes needed.
GlassGroupedSection now automatically injects GlassDividers between tiles
with smart leading-indent detection (56px if the preceding tile has a leading
widget, 16px otherwise). The last tile never gets a trailing divider.
Standalone column layouts — compose GlassDivider explicitly:
// Before:
Column(children: [
GlassListTile(title: Text('A')), // showDivider: true (default)
GlassListTile(title: Text('B'), isLast: true), // suppresses divider
])
// After (standard Flutter composition pattern):
Column(children: [
GlassListTile(title: Text('A')),
GlassDivider(indent: 16),
GlassListTile(title: Text('B')),
])
This aligns with Flutter's own ListTile + Divider composition model.
✨ New — GlassTabBar.inline #
A compact glass tab bar for pinned content-filter sections — sits fixed between a page header and its scrollable list, not inside the scroll view itself.
Performance note:
GlassQuality.premiumre-runs the full shader pipeline whenever the backdrop changes. Placing the bar inside a scroll view invalidates the backdrop on every scroll frame. Pin it outside the scrollable region or drop toGlassQuality.standardif embedding inside a list.
GlassTabBar.inline(
tabs: const [
GlassTab(label: 'For You'),
GlassTab(label: 'Following'),
GlassTab(label: 'New'),
],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
)
Key differences from GlassTabBar.bottom:
- Zero padding — sits flush in its parent container.
- Compact height (40px) with a full stadium shape (
barBorderRadius: 100, clamped toheight/2). - Indicator pill automatically matches the track corner radius (
indicatorBorderRadiusdefaults tobarBorderRadius). - Indicator expansion
h:12, v:10— same horizontal weight asGlassSegmentedControlandGlassTabBar.bottom, +2px vertical to compensate for the shorter bar height giving the glass pill proportional visual mass. indicatorPinchStrength: 0.4— aligned with all other pill controls.- Magnification disabled (
1.0x) — text labels never grow on selection. - Text-only tabs render with correct vertical centering (no icon slot consuming space).
extraButtonnot supported (structural navigation feature only).
All standard physics parameters (indicatorPinchStrength, indicatorExpansion,
indicatorSettings, quality) are still fully configurable.
See the updated Indicator Parity demo (indicator_parity_demo.dart) where
GlassTabBar.inline now appears alongside the other five pill widgets — including
a text-only variant (40px) and an icon + text variant (52px) — with live tuning sliders.
0.19.7 #
🐛 Bug Fixes #
-
GlassTabBar.bottom/GlassBottomBar— fixes RTL support (#143, @naeemeltaief). The pill and tap/drag hit-testing now correctly align with the visually reversed tab order under RTL. The bottom layout normalises the tab data, selected index and callback for RTL, and pins only the two inner tabRows to LTR — leavingindicatorExpansion,tabPaddingand all text to resolve against the ambient direction as expected. Consumers no longer need to forceDirectionality.ltrand reverse tabs by hand. +2 tests. -
AnimatedGlassIndicator— restores the resting selection pill inside aGlassContainer(#144, @jfhair). The background pill was permanently hidden whenever an ancestor setavoidsRefraction, which is a steady-state layout flag onGlassContainer— not a transient capture signal. The guard was incorrect and is removed. The pill now renders correctly in all contexts. No API change. +1 test.
0.19.6 #
✨ New — GlassLargeTitle + GlassLargeTitleController #
First-class iOS 26 large-title + search-bar collapse. Replaces manual
ScrollController + setState wiring with a single controller and two
widgets.
Two-phase collapse #
- Phase 1 — large title fades out (
Curves.easeIn, rubber-band overscroll stretch). - Phase 2 — optional inline search bar collapses under the nav bar immediately after.
New API #
GlassLargeTitle— sliver widget. Drop it as the first sliver in anyCustomScrollView.searchBar: Widget?— Phase 2 collapse (e.g.GlassSearchBar).trailing,fontSize,fontWeight,letterSpacing,padding,searchBarPadding,color.
GlassLargeTitleController— owns theScrollController, exposescollapseProgressandsearchBarCollapseProgress. Self-calibrates to Dynamic Type viareportMeasuredHeight/reportSearchBarHeight.GlassAppBar.largeTitleController— new optional param. Bar title cross-fades in as the large title collapses. Non-breaking.
final _ctrl = GlassLargeTitleController(); // dispose in State.dispose()
// Phase 1 only
GlassLargeTitle(text: 'Chats', controller: _ctrl)
// Phase 1 + 2
GlassLargeTitle(
text: 'Messages',
controller: _ctrl,
searchBar: GlassSearchBar(placeholder: 'Search', onChanged: (_) {}),
)
Demo #
- Pattern 2 updated to new API. Pattern 7 (Large Title + Search Bar) added.
- Apple Messages demo migrated from manual
AnimatedOpacitytoGlassLargeTitle.
+10 tests. 2,291 total, all passing.
⚡ Performance — GlassBottomBar Indicator (Impeller) #
GlassBottomBarindicator — eliminates the liveBackdropFilterLayeron Impeller premium, replacing it with a deterministictoImageSynccapture path. Fixes the opaque-white indicator rendering on physical iOS (#99) and improves drag performance by removing a redundant compositor pass.
🔧 GlassGroupedSection — Header/Footer styling #
- Header and footer text is now styled automatically via
DefaultTextStyle(CupertinoColors.secondaryLabel, 13pt) — callers no longer need to style them manually. - Margin is now applied via
Paddingwrapping the full section rather than on the innerGlassCard, fixing card-edge clipping when a header or footer is present. - Import changed to
cupertino.dartfor correctCupertinoColorsresolution.
📝 Dart doc improvements #
GlassContainer,GlassCard,GlassGroupedSection,GlassSegmentedControl— added ⚠️ Anti-Pattern sections documenting the glass-in-glass restriction: placing interactive glass controls inside a container degrades refraction and clips jelly animations.
0.19.5 #
-
LiquidGlassSettings— addsplatformViewFallbackColor(#138, @jfhair). SplitsbackerColor's dual role:backerColorremains the aesthetic backer pad; the new field controls theuBackgroundFallbackshader uniform (PlatformView fill). Fully backwards-compatible — defaults tonull, falling back tobackerColor. -
GlassModalSheet— removes the interiorBoxShadowthat bled through the glass as a vignette (#137, @jfhair). Elevation now flows viaLiquidGlassSettings.shadowElevation. PassshadowElevation: 0to disable entirely.
0.19.4 #
✨ Enhancements — GlassButtonGroupItem.menu & GlassPullDownButton improvements #
GlassButtonGroupItem.menu — whole-pill liquid glass morph #
Adds a new GlassButtonGroupItem.menu named constructor that turns any item in a
GlassButtonGroup.icons pill into a pull-down menu trigger.
When tapped, the entire pill morphs into the menu — the full GlassButton.custom
shell becomes the GlassMenu trigger, so the whole group shape liquefies and expands
into the menu card. This matches the iOS 26 GlassEffectContainer morph pattern where
the source shape, not just the tapped slot, participates in the transition.
GlassButtonGroup.icons(
items: [
GlassButtonGroupItem(icon: Icon(CupertinoIcons.chart_bar), onTap: () {}),
GlassButtonGroupItem(icon: Icon(CupertinoIcons.clock), onTap: () {}),
GlassButtonGroupItem.menu(
icon: Icon(CupertinoIcons.ellipsis),
menuItems: [
GlassMenuItem(title: 'Add to Watchlist', icon: Icon(CupertinoIcons.star), onTap: () {}),
GlassMenuItem(title: 'Share', icon: Icon(CupertinoIcons.share), onTap: () {}),
GlassMenuDivider(),
GlassMenuItem(title: 'Remove', icon: Icon(CupertinoIcons.trash), isDestructive: true, onTap: () {}),
],
menuAlignment: GlassMenuAlignment.topRight, // optional
menuWidth: 200, // optional, default 200
),
],
)
Notes:
- Only the first
GlassButtonGroupItem.menuin the list is used as the menu trigger; any subsequent menu items are treated as plain tap items. - Accepts both
GlassMenuItemandGlassMenuDivider, matchingGlassMenu.itemsdirectly. - Non-menu siblings in the group continue to fire their own
onTapindependently. - Works with both
Axis.horizontalandAxis.verticalgroups.
Alternatively — group + standalone GlassPullDownButton
For cases where the menu trigger is a separate, visually distinct action from the
group (e.g. a trailing overflow button next to a row of chart controls), compose a
GlassButtonGroup alongside a standalone GlassPullDownButton. The pull-down
button morphs independently and fully, with no pill residue:
Row(
children: [
GlassButtonGroup.icons(
items: [
GlassButtonGroupItem(icon: Icon(CupertinoIcons.chart_bar), onTap: () {}),
GlassButtonGroupItem(icon: Icon(CupertinoIcons.clock), onTap: () {}),
],
),
SizedBox(width: 8),
GlassPullDownButton(
icon: Icon(CupertinoIcons.ellipsis),
menuAlignment: GlassMenuAlignment.topRight,
items: [
GlassMenuItem(title: 'Add to Watchlist', icon: Icon(CupertinoIcons.star), onTap: () {}),
GlassMenuItem(title: 'Share', icon: Icon(CupertinoIcons.share), onTap: () {}),
GlassMenuDivider(),
GlassMenuItem(title: 'Remove', icon: Icon(CupertinoIcons.trash), isDestructive: true, onTap: () {}),
],
),
],
)
.menu item (shared pill) |
group + standalone | |
|---|---|---|
| All actions in one pill | ✅ | ❌ |
| Entire pill morphs | ✅ | — |
| Menu button morphs independently | — | ✅ |
| Best for | Overflow within a related set | Separate trailing action |
GlassPullDownButton improvements #
itemswidened toList<Widget>— now acceptsGlassMenuDivideralongsideGlassMenuItem. Source-compatible: existingList<GlassMenuItem>code compiles and behaves identically. TheonSelectedcallback is applied only toGlassMenuIteminstances.menuAlignmentexposed — newGlassMenuAlignment?parameter forwarded to the underlyingGlassMenu. Defaults tonull(auto-detect from screen position) — fully backward-compatible.
🐛 Bug Fixes — PlatformView Frost Halo & GlassButton Dispose-Race (#134, #135) #
Both fixes contributed by @jfhair.
LiquidOval rectangular blur halo over a PlatformView (#134) #
Problem: Any glass surface with a LiquidOval shape (the default for GlassButton,
GlassIconButton, the collapsed search/dismiss pill, and GlassBottomBarExtraButton)
rendered a rectangular blur halo when platformViewBackdrop: true routed it through the
_FrostedFallback BackdropFilter path. The halo matched the widget's bounding box rather
than its circular outline.
Root cause: Flutter engine PR #177551 (3.41+) forwards ClipRRect clip data to the iOS
PlatformView mutator stack, allowing a descendant BackdropFilter to be bounded correctly
over a hybrid-composed view. The same forwarding does NOT apply to ClipPath — and
LiquidOval (unlike LiquidRoundedSuperellipse) was routing through ClipPath, leaving
the BackdropFilter unclipped.
Fix: _ShapeClip now accepts a platformViewBackdrop flag. When set, any shape whose
border radius can be expressed as a BorderRadius (including LiquidOval →
circular(9999), LiquidRoundedRectangle, and their vertical variants) is routed through
ClipRRect instead of ClipPath. The clip is then forwarded to the PlatformView mutator
stack and the frost is bounded cleanly to the shape. Off a PlatformView the original
ClipPath is used (true ellipse). The flag is threaded through all _ShapeClip call sites
in _FrostedFallback — the blur body, the content clip, and the specular rim — as well as
the backer dimming pad in AdaptiveGlass._wrapWithBacker.
Completes the partial fix shipped in 0.19.3 and fully resolves the rectangular-blur regression first reported in #79.
GlassButton crash when disposed mid-press (#135) #
Problem: Tapping a GlassButton that is removed by the very tap that activates it
(e.g. a collapsed bar restore button that expands the bar and disposes itself) could throw:
AnimationController.reverse() called after AnimationController.dispose()
(assert _ticker != null)
A queued pointerUp or pointerCancel was still dispatched to the now-disposed
RenderPointerListener, and the press handler called _saturationController.reverse()
after dispose().
Fix: All six tap/pointer press handlers in _GlassButtonState now guard on !mounted
before touching _saturationController. A disposed State always has mounted == false,
so the handler bails safely without touching the controller.
Surfaces frequently in apps that morph or collapse a bar on the tap of a glass control over a PlatformView (the pattern introduced in #127/#79).
0.19.3 #
🐛 Bug Fixes — Search Pill Colors & PlatformView Compositing #
- Fixed an issue where the
SearchPillicon colors (search, mic) would incorrectly render as black when using dark glass (glassColor: Colors.black26) over an iOS PlatformView. The root cause was that the color resolution used the OS system brightness (light) instead of the glass brightness (dark), causingCupertinoDynamicColor.labelto always resolve to its light-mode black variant. The widget now resolves colors throughGlassTheme.brightnessOf()— the package's single brightness authority — and explicitly passes the resolved color viaIconThemeDatato guarantee white glyphs on dark glass regardless of system brightness. - Mitigated a Flutter engine clipping bug over PlatformViews by ensuring
LiquidRoundedSuperellipseandLiquidOvalcorrectly clip their bounds using an outerClipRRectwhen rendering over an iOS PlatformView, preventing blur bleed outside the circular button shape. - Updated the
google_maps_demoto correctly demonstrate how to configureselectedIconColor,unselectedIconColor, andsearchIconColorfor dark glass bottom bars to ensure all tab elements remain visible over the map layer.
0.19.2 #
🐛 Bug Fixes — PlatformView Gesture & Rendering #
This release resolves a pair of related issues that caused the glass bottom bar to freeze and render incorrectly when floating over an iOS PlatformView (e.g. a Mapbox map). All three fixes were contributed by @jfhair via detailed PRs that included root-cause analysis, regression tests, and working reproductions. Many thanks for the exceptional quality of this contribution.
Gesture freeze over an iOS PlatformView (#127) #
Problem: GlassTabBar.searchable (and GlassBottomBar) would freeze when
the draggable indicator was dragged or tapped while the bar floated over an iOS
PlatformView. The freeze was permanent until the widget was rebuilt or disposed.
Root cause: iOS reconstructs the platform-view clip chain whenever a clip
layer is added or removed mid-gesture. The engine responds by cancelling the
active touch, which left the bar's GestureDetector recognizer wedged — it
never received a terminal callback (onDragEnd / onDragCancel) and stopped
responding to input. The most frequent trigger was the indicator's innerBlur
frost layer unmounting at drag-start (a clip-add/remove cycle).
Fix — two-part:
- Primary fix (PR #127): The indicator's frost
ClipRRect + BackdropFilterlayer now stays mounted across the full drag lifecycle by tracking a persistentbool _frostMountedflag inAnimatedGlassIndicator. This eliminates the clip-add/remove cycle that was triggering the iOS clip-chain reconstruction. - Backstop fix (PR #127):
TabBarDragGestureMixingains agestureEpochcounter, a_gestureActiveliveness flag, and a rawListeneron theGestureDetectorsubtree. If the raw pointer-up or pointer-cancel arrives while_gestureActiveis stilltrue(i.e. the terminal callback was silently dropped by the platform-view gesture arena),recoverIfGestureStuckis called: it selects a fallback tab, bumpsgestureEpoch(forcing theGestureDetectorto be torn down and recreated viaValueKey), and clears the stuck state. Covers both the tap-without-cancel and drag-start-without-end freeze signatures.
PlatformView backdrop routing (#128) #
Problem: Setting platformViewBackdrop: true on a bar had no visible effect
for the glass indicator or bar body — the premium/standard Impeller shader paths
read a captured backdrop that excludes hybrid-composed PlatformViews, so the
glass rendered inert (opaque black or clear) over a map.
Fix: AdaptiveGlass now routes any surface with platformViewBackdrop: true
to the frosted fallback (_FrostedFallback) regardless of the requested quality
tier. The frosted fallback uses a live BackdropFilter, which correctly blurs
hybrid-composed PlatformViews. _FrostedFallback also overrides the
isInteractive blur-omission that would otherwise skip the blur for interactive
indicator surfaces — over a PlatformView the live blur must always run.
Premium glass goes black over a PlatformView — backerColor fallback (#129) #
Problem: At GlassQuality.premium over a PlatformView, the Impeller shader
sampled a captured backdrop that contained only transparent black where the
PlatformView sat. With no real background pixels to refract, the glass lens
rendered black.
Fix: A new uBackgroundFallback (vec4) uniform was added to
liquid_glass_final_render.frag. The shader composites the fallback colour
over the captured backdrop using a standard over blend weighted by the
backdrop's own alpha — where the backdrop is real (alpha ≈ 1) it is left
untouched; where it is transparent black (alpha ≈ 0, i.e. over a PlatformView)
the fallback fills in. backerColor from LiquidGlassSettings is bound to
this uniform at render time, giving the premium lens a solid colour to refract
instead of transparent black.
Extra button platformViewBackdrop threading (follow-up, this release) #
GlassTabBarExtraButton previously ignored the bar's platformViewBackdrop
flag — the internal BottomBarExtraBtn widget was not forwarding it to the
underlying GlassButton, so the extra button continued to use the inert shader
path even when the rest of the bar correctly used the frosted fallback.
platformViewBackdrop is now threaded through BottomBarExtraBtn and both
call sites (TabBarBottomLayout, TabBarSearchableLayout) to GlassButton.
0.19.1 #
🛡️ Stability Improvements #
Addresses production crash and ANR reports seen with v0.19.0 on Flutter 3.44.2 (tracked in flutter/flutter#187140). These are exposure-window mitigations — the root cause is a Flutter engine issue and requires an engine-level fix.
Changes #
GlassEffect & LightweightLiquidGlass — lifecycle-aware Ticker suspension
Both state classes now implement WidgetsBindingObserver and halt background-capture
Tickers during AppLifecycleState.inactive, paused, and hidden. Captures restart
one frame after resumed. This reduces GPU texture activity during surface transitions
(rotation, split-screen, keyboard resize) which is the window where engine-level
crashes and ANRs are most likely to occur.
GlassEffect & LightweightLiquidGlass — _isDisposed guard
A _isDisposed flag prevents Ticker callbacks and async .then() continuations
from accessing GPU resources after dispose() has completed, guarding against
post-frame-callback / dispose races during rapid navigation.
LiquidGlassWidgets.initialize() — faster startup
The Impeller pipeline warm-up is no longer awaited inside initialize(). It now
runs after the first frame via addPostFrameCallback, removing a ~16ms delay from
the startup critical path. Shader disk-loads are still awaited as before.
Debug log cleanup
Removed stale debugPrint success messages from GlassEffect and LightweightLiquidGlass
shader pre-warm paths (✓ Shader precached, ✓ Created unique shader instance). These
fired on every debug startup for every app using the package. Failures still surface via
the existing [LightweightGlass] Pre-warm failed: error log. The [LiquidGlass]
startup bracket (Initializing... / Initialization complete.) and the
PerformanceMonitor started log are retained as actionable developer information.
0.19.0 #
💥 Breaking: Pre-v1.0 Public API Cleanup #
A pre-release naming audit to establish consistent, idiomatic conventions before v1.0 locks the API.
Renames #
| Old | New | Reason |
|---|---|---|
GlassBottomBarExtraButton |
GlassTabBarExtraButton |
Tracks parent rename GlassBottomBar → GlassTabBar |
GlassGroupItem |
GlassButtonGroupItem |
Mirrors Flutter's DropdownMenuItem pattern |
SheetState |
GlassSheetState |
Prevents collision with Material 3 sheet infrastructure |
SheetMode |
GlassSheetMode |
Same — too generic as a bare name |
FillTransition |
GlassFillTransition |
Too generic as a bare name |
ExtraButtonPosition |
GlassExtraButtonPosition |
Ambiguous without prefix |
Migration: A
@Deprecatedtypedef forGlassBottomBarExtraButtonis provided. All other old names will produce compile errors — migration is mechanical find-and-replace.
GlassSegment — new concrete class #
GlassSegment was previously a typedef alias for GlassTab. It is now a proper class with a focused API for GlassSegmentedControl:
// GlassSegment — for GlassSegmentedControl only
GlassSegment({ Widget? icon, String? label, String? tooltip, String? semanticLabel, bool enabled = true })
// GlassTab — for GlassTabBar.bottom() and GlassTabBar.searchable()
GlassTab({ Widget? icon, Widget? activeIcon, String? label, Color? glowColor, double? thickness })
Fields like activeIcon, glowColor, and thickness are navigation-specific and only exist on GlassTab. GlassSegment adds tooltip and enabled (with built-in disabled rendering at 38% opacity).
Barrel hygiene #
Internal types (SheetSnapshot, SheetGeometry, GesturePhase, GestureArena, FrozenState) are no longer accessible from the package barrel. These were implementation details that leaked through part file exports.
⚡ Performance #
- Shader:
interactive_indicator.frag— replacedpow()calls with multiply chains; collapsed duplicate rim pass; zero transcendental functions in highlight path. - Shader:
liquid_glass_final_render.frag—⁶√xcomputed via sqrt cascade (3 SFU vs 2 transcendentals);sceneSDFsamples reduced from 5 → 4. - Dart:
resolveAdaptiveRadiusscoped toMediaQuery.viewPaddingOf+MediaQuery.sizeOf— glass widgets no longer rebuild on keyboard or unrelatedMediaQueryDatachanges. - Dart: Searchable tab bar and
GlassSegmentedControlspring animations now useListenableBuilderscoped to the indicator subtree. Verified on-device: zeroState.build()calls during 120Hz spring animation.
🐛 Fix #
LiquidGlassWidgets.initialize()now pre-warms all four shaders —liquid_glass_geometry_blended.fragandliquid_glass_final_render.fragwere previously lazy-loaded, causing first-frame jank.GlassSegment.enabled = falsenow blocks tap/tapDown — disabled segments rendered at 38% opacity but still firedonSegmentSelectedin both fixed-width and scrollable modes. Tap andonTapDownhandlers now early-return when the target segment is disabled.
0.18.6 #
🐛 Fix: Glass widgets now honour app ThemeMode, not OS dark mode #
Resolves a class of UI inconsistency where glass widgets incorrectly read the device/OS brightness instead of the app's brightness. The most visible symptom was GlassBottomTabBar shadows disappearing when the device was in Dark Mode but the app was pinned to Light Mode via MaterialApp(themeMode: ThemeMode.light).
Root cause #
Every glass widget that needed to decide between light/dark colours or shadow visibility called either CupertinoTheme.of(context).brightness or MediaQuery.platformBrightnessOf(context) directly. Both of these APIs fall back to the OS/device setting and are blind to MaterialApp.themeMode.
Fix: Centralised brightness cascade #
A new GlassTheme.brightnessOf(context) authority now governs all brightness decisions in the library. It resolves via a four-level cascade:
GlassThemeData.brightness— new field; an explicit developer override pinned in theGlassThemewidget tree (highest priority).CupertinoThemeData.brightness— explicit Cupertino pin (non-null only; intentional opt-in).Theme.maybeBrightnessOf(context)— MaterialThemeMode.light/.dark/.system, honouringMaterialApp.themeMode.MediaQuery.platformBrightnessOf(context)— OS/device setting (safe fallback).
Changes #
- New:
lib/utils/glass_brightness.dart—resolveGlassBrightness(context)utility (package-private). - New field:
GlassThemeData.brightness— explicit brightness override for fine-grained glass-subtree control. Accepted by both the default andGlassThemeData.simple()constructors. - New method:
GlassTheme.brightnessOf(context)— the single, mandatory brightness authority for the entire library. - Fixed: Shadow suppression in
GlassBottomTabBar,GlassSearchableBottomBar,AdaptiveLiquidGlassLayer, andAdaptiveGlass(_FrostedFallback). - Fixed: Shader
backdropLumaproxy inGlassEffect(controls glass strength in the GPU path). - Fixed: Light/dark colour selection in 20+ widget files across
interactive/,containers/,input/,overlays/, andsurfaces/layers.
Tests #
Three new test files cover every level of the cascade:
test/utils/glass_brightness_test.dart— unit tests forresolveGlassBrightness.test/theme/glass_theme_brightness_test.dart—GlassTheme.brightnessOfintegration tests including the canonical regression scenario.test/theme/glass_theme_data_brightness_test.dart—GlassThemeData.brightnessoverride field,copyWith, equality, and backward-compat tests.
0.18.5 #
🔧 Corrected minimum SDK constraint — Flutter ≥ 3.41.0 #
- Fix: Raised the minimum Flutter constraint to
3.41.0. ThefilterQualityparameter onFragmentShader.setImageSampler()was actually introduced in Flutter 3.41.0 (commitadd442b29c), not 3.24.0 as previously stated. This prevents users on 3.38.x from failing at compile time. - Reverted: Raised the internal
metaconstraint back to^1.18.0since Flutter 3.41.0 guarantees this version is available.
0.18.4 #
- Fix: Loosened the
metadependency constraint to^1.12.0(instead of^1.18.0) to avoid pub resolution conflicts for users on older Flutter SDKs whereflutter_testis bound tometa 1.17.0.
0.18.3 #
✨ Per-state label text style on bottom bars — selectedLabelStyle / unselectedLabelStyle #
Adds selectedLabelStyle / unselectedLabelStyle (TextStyle?) to GlassTabBar.bottom, GlassTabBar.searchable, and the deprecated GlassBottomBar / GlassSearchableBottomBar.
This complements the selectedLabelColor / unselectedLabelColor parameters by letting callers set the selected/unselected label's font family, weight, and letter-spacing per state — needed to match Apple's tab bar, where the selected label is heavier than a single shared textStyle can express.
The per-state style is merged over the base label style, so it overrides only the fields it sets and keeps the resolved per-state label color unless the style provides its own. Both default to null → existing behavior unchanged.
Also fixes a related precedence bug: an explicit selectedLabelColor / unselectedLabelColor was silently dropped whenever a textStyle was also supplied (the per-state color only fed the fallback style). It now overrides the base color — including a color baked into textStyle — while textStyle-only callers are unaffected.
✨ innerBlur — Apple-style rest-blur behind the selected tab #
The innerBlur parameter on GlassTabBar.bottom/.searchable (and the deprecated GlassBottomBar/GlassSearchableBottomBar shims) now renders. It was declared and threaded bar→internal, but never forwarded to the indicator, and AnimatedGlassIndicator had no implementation. This wires it through the tab-bar internals and adds the rest-gated BackdropFilter.
It paints a backdrop blur behind the resting selected pill — the iOS 26 "frost at rest" look — with the sigma scaled by the pill's resting opacity, so the frost is full when settled and fades out as it morphs into the liquid-glass lens during a drag/tap (motion stays crisp).
0.0(default) disables it — no behavior change for existing callers.- Only the background-painting indicator is affected (reads through a translucent
indicatorColor).
✨ platformViewBackdrop on the public glass widgets #
Follows up #94 by exposing the premium-over-PlatformView flag on the remaining public widgets — GlassContainer, GlassButton, GlassIconButton, GlassButtonGroup, GlassMenu, and GlassModalSheet — so apps can render premium glass cleanly over an iOS PlatformView (e.g. a Mapbox MapWidget) for any control, not just the bottom bar.
Each gets an explicit platformViewBackdrop parameter defaulting to false (zero overhead / no behavior change for callers that don't need it), forwarded to the underlying AdaptiveGlass. For GlassModalSheet the flag threads through GlassModalSheetScaffold and the sheet state down to the _SheetLayout's glass. Adds widget tests covering the simple-widget forwards.
🧹 Removed — GlassTintBlend (a no-op since 0.17.0) #
GlassTintBlend (added in #107) and the LiquidGlassSettings.tintBlend field have been removed.
Since the 0.17.0 shader rewrite, tintBlend was never wired into the renderer — it was packed into no uniform, so setting it had no effect: every surface used the automatic chroma-gated blend regardless of the value. We only caught this during real-device tuning, after all the related PRs had already landed.
The automatic behavior is unchanged. applyGlassColor still picks luminosity-preserving blending for chromatic tints and flat blending for achromatic tints. Recipes that previously passed tintBlend: flat for achromatic (white / grey / near-black) tints render identically, because the chroma gate already resolves those to the flat path.
Breaking, but inert: code that passed LiquidGlassSettings(tintBlend: …) must drop the argument. No rendered output changes.
🔧 SDK constraints bump (corrected in 0.18.5) #
Raised minimum Flutter to >=3.24.0 — this was incorrect. The actual minimum is 3.41.0, corrected in 0.18.5.
0.18.2 #
Rendering Quality #
-
Fix: Eliminated stair-step aliasing on
AnimatedGlassIndicator/GlassEffectpill edges during press animations. Root cause:FragmentShader.setImageSampler()defaults toFilterQuality.none(Nearest-Neighbor). The 4 % press-scale animation was block-replicating geometry texels into 2×2 stair-step patterns visible as jagged fringe on the pill rim. Resolution: PassfilterQuality: FilterQuality.mediumto everysetImageSampler()call inliquid_glass_render_object.dart,glass_effect.dart, andlightweight_liquid_glass.dart. Zero GPU cost — hardware bilinear filtering happens in a single texel-unit clock cycle. -
Fix: Eliminated 2×2 blocky normal artifacts on glass pill edges (all platforms, most visible on iOS/macOS Metal). Root cause: Metal's
dFdx/dFdyevaluate in 2×2 pixel quads, so all four neighbours share one gradient vector. At the high-contrast white rim of the pill this produces a coarse stair-step normal map that manifests as jaggy rainbow banding regardless of scale. Resolution: Replaced hardware derivatives with per-pixel central finite differences inliquid_glass_geometry_blended.frag(dx = sceneSDF(p + 0.5) − sceneSDF(p − 0.5)). Costs 4 extra SDF evaluations per geometry pixel — negligible on a cached, one-shot geometry pass. -
Fix: Restored full rim brightness after the anti-aliasing band was widened. Root cause: The previous asymmetric
smoothstep(-smoothing, 0.0, sd)placed the mathematical pill boundary (sd = 0) at the dark end of the alpha ramp (alpha = 0). The rim-lighting peak lives exactly atsd = 0, so it was multiplied by zero and rendered invisible. Resolution: Centred the smoothstep around the boundary —smoothstep(smoothing * 0.5, -smoothing * 0.5, sd)— sosd = 0maps to alpha = 0.5. This is the canonical SDF anti-aliasing formulation and restores maximum rim brightness with no other visual side-effect. -
Fix: Eliminated backdrop texture wrap-around artifacts during
LiquidStretchscaling and jelly overshoot. Root cause: Impeller's default texture sampler wrap mode isRepeat. A fragment slightly outsideuGeometrySize(e.g. during a spring overshoot) produced ageometryUVmarginally above 1.0; the sampler wrapped it to near-0.0, sampling the opposite edge of the SDF and producing inverted normals and extreme chromatic aliasing. Resolution: ClampgeometryUVto[0, 1]before the texture fetch. Clamped-edge pixels have near-zero SDF alpha and are discarded by the existinggeometryData.a < 0.01early-out — no separate bounds-check branch required. -
Fix: Eliminated chromatic wrap artifacts during jelly overshoot in
interactive_indicator.frag. Root cause: When the indicator pill overshoots itsRepaintBoundarybounds, out-of-boundstextureBilinearsample points could trigger the same Repeat-mode wrap in the background texture. Resolution: Explicitly clamp all four bilinear sample points to[0, physSize − 1]before thetexture()fetch. -
Fix: Eliminated jagged/pixelated stair-step artifacts on
AnimatedGlassIndicatorpill edges whenindicatorPinchStrength > 0. Root cause:BackdropFilterLayerimplicit samplers are bound toFragmentShaderas Nearest-Neighbor with no Dart API to override it (Flutter Issue #139887). Continuous sub-pixel UV shifts from the lens pinch and chromatic aberration were snapping to integer texels, producing blocky rainbow fringes on high-contrast backgrounds. Resolution: Added atextureBilinearhelper toliquid_glass_final_render.fragthat performs a standard 4-texel bilinear interpolation in GLSL, restoring perfectly smooth sub-pixel background sampling. The geometry texture (uGeometryTexture) is intentionally excluded — its pixel-aligned SDF data must not be softened. -
Fix: Eliminated pixelation on the interactive indicator pill (
GlassSegmentedControl,GlassEffect). Two compounding causes: (1) the background texture was previously captured atpixelRatio: 1.0, so each texel covered a 3×3 block of physical pixels on a 3× Retina display; (2) Impeller'ssetImageSampler()binding defaults to Nearest-Neighbor, snapping continuous UV offsets from edge refraction to these large texels. Resolution: Background capture now uses the device's full DPR. AtextureBilinearGLSL helper replaces all rawtexture()calls ininteractive_indicator.frag. ~250 KB additional GPU texture memory; sub-0.1 ms additional GPU time per frame — negligible on any modern device. -
Fix: Resolved intermittent Metal API Validation abort on iOS (
GlassQuality.premium) — missing buffer bindings foruWhiten,uWhitenGated, anduPinchStrength(#121). All shader uniforms (slots 0–20) are now written atomically on every paint frame, preventing a stale or zero-initialisedFragmentShadersnapshot from reaching the Metal draw call. Affected:GlassTabBar.bottom(quality: GlassQuality.premium)with an animating indicator. No API changes.
Notes #
- Note (Flutter engine limitation): The
textureBilinearworkaround inliquid_glass_final_render.frag(4-tap bilinear in GLSL) remains necessary for backdrop sampling because Impeller binds the implicitBackdropFilterLayersampler as Nearest-Neighbor with no Dart API to overrideFilterQuality. A Flutter engine feature request to expose sampler filter quality for backdrop layers has been filed at Flutter Issue #188365. Once resolved, the GLSL workaround can be replaced with a singletexture()call.
Chore #
- Chore: Removed unreachable early-out branch in
liquid_glass_final_render.frag— theif (any(lessThan(geometryUV, ...)))check afterclamp(geometryUV, 0.0, 1.0)could never fire. Replaced with a single consolidated comment explaining how the clamp and the downstream alpha check together handle both the Impeller Repeat-mode and clipExpansion cases. - Chore: Removed dead code from
render.glsl(computeY,getHeight,calculateLighting,calculateRefraction,renderLiquidGlass,debugNormals) — functions superseded by the inline logic inliquid_glass_final_render.frag. Reduces compiled shader binary size.
0.18.1 #
- Hotfix: Resolved missing coverage in layout engines and segmented controls.
- Hotfix: Fixed package analysis warnings due to unused local variables and unnecessary imports in test files.
0.18.0 #
🏗️ Unified Navigation API — iOS 26 Alignment #
This release consolidates the widget API to map 1:1 with Apple's iOS 26 control vocabulary. Two v1-era widgets are deprecated (see Migration below), and GlassTabBar becomes the single source of truth for all tab-navigation work.
⚠️ Breaking Change: Android Bottom Bar Padding #
GlassScaffold now automatically manages the Android system navigation bar padding for bottomBar. If you previously added manual Padding or SafeArea around your bottom bar to prevent it from slipping behind the Android navigation buttons, please remove it to avoid double-padding.
New: GlassTabBar.bottom() — iOS 26 UITabBar equivalent #
Named constructor for bottom navigation bars. Full liquid glass layer, jelly physics pill indicator, MaskingQuality dual-layer icon rendering, and optional dividerSettings.
GlassTabBar.bottom(
tabs: [
GlassTab(icon: Icon(Icons.home), label: 'Home'),
GlassTab(icon: Icon(Icons.search), label: 'Search'),
GlassTab(icon: Icon(Icons.person), label: 'Profile'),
],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
)
New: GlassTabBar.searchable() — UITabBar + morphing search #
Named constructor combining bottom navigation with a morphing glass search pill. Identical API to GlassTabBar.bottom() with additional searchBarConfig and controller parameters.
GlassTabBar.searchable(
tabs: [...],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
searchBarConfig: GlassSearchBarConfig(hintText: 'Search...'),
controller: _tabBarController,
)
New: GlassSegmentedControl — icon support + scrollable mode #
Icon + label support (fixed mode)
segments now accepts List<GlassTab> instead of List<String>. Each segment can render:
- Label only —
GlassTab(label: 'Weekly') - Icon only —
GlassTab(icon: Icon(Icons.photo)) - Icon + label —
GlassTab(icon: Icon(Icons.photo), label: 'Photos')(icon above label)
This matches iOS 26 UISegmentedControl which has supported UIImage segments since early iOS.
Scrollable mode — 100% parity with original GlassTabBar(isScrollable: true)
🚨 Removed: GlassTabBar() inline constructor #
The default GlassTabBar() constructor has been removed. GlassTabBar is now exclusively used for structural bottom navigation (GlassTabBar.bottom() and GlassTabBar.searchable()).
Migration:
For all inline tab bars, pill menus, or scrollable tag lists, use GlassSegmentedControl() or GlassSegmentedControl.scrollable(). They provide 100% feature parity with the old inline GlassTabBar.
- GlassTabBar(
- tabs: const [
- GlassTab(label: 'A'),
- GlassTab(label: 'B'),
- ],
- selectedIndex: _selectedIndex,
- onTabSelected: (i) => setState(() => _selectedIndex = i),
- )
+ GlassSegmentedControl(
+ segments: const [
+ GlassSegment(label: 'A'),
+ GlassSegment(label: 'B'),
+ ],
+ selectedIndex: _selectedIndex,
+ onSegmentSelected: (i) => setState(() => _selectedIndex = i),
+ )
New GlassSegmentedControl.scrollable() named constructor for category filter tabs (6+ items). Internally uses ScrollableSegmentContent — a dedicated widget that owns scrollable pill physics, gesture handling, and 3-layer rendering. Structurally identical to the old inline GlassTabBar(isScrollable: true), now correctly located in the interactive widget family.
// Fixed (UISegmentedControl — equal width, 2–6 items)
GlassSegmentedControl(
segments: const [
GlassSegment(label: 'All'),
GlassSegment(icon: Icon(Icons.photo), label: 'Photos'),
GlassSegment(label: 'Videos'),
],
selectedIndex: _selectedIndex,
onSegmentSelected: (i) => setState(() => _selectedIndex = i),
)
// Scrollable (category filter tabs — natural width, 7+ items)
GlassSegmentedControl.scrollable(
segments: List.generate(12, (i) => GlassSegment(label: 'Category ${i + 1}')),
selectedIndex: _selectedIndex,
onSegmentSelected: (i) => setState(() => _selectedIndex = i),
)
Architecture: Dependency inversion #
All tab-bar layout logic now lives in dedicated layout files:
| File | Owns |
|---|---|
interactive/shared/scrollable_segment_content.dart |
ScrollableSegmentContent — scrollable pill + gesture engine (used by GlassSegmentedControl.scrollable) |
interactive/shared/segmented_control_internal.dart |
SegmentedControlContent — fixed-width pill + gesture engine (used by GlassSegmentedControl) |
surfaces/shared/tab_bar_bottom_layout.dart |
TabBarBottomLayout — full glass bottom shell |
surfaces/shared/tab_bar_searchable_layout.dart |
TabBarSearchableLayout — search morph shell |
GlassTabBar dispatches to these shells. GlassBottomBar and GlassSearchableBottomBar are now zero-logic shims that delegate to the same shells.
Deprecated — removal in v1.0 #
GlassBottomBar → GlassTabBar.bottom()
// BEFORE
GlassBottomBar(
tabs: [GlassBottomBarTab(icon: Icon(Icons.home), label: 'Home')],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
)
// AFTER
GlassTabBar.bottom(
tabs: [GlassTab(icon: Icon(Icons.home), label: 'Home')],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
)
GlassSearchableBottomBar → GlassTabBar.searchable()
// BEFORE
GlassSearchableBottomBar(tabs: [...], ...)
// AFTER
GlassTabBar.searchable(tabs: [...], ...)
GlassTabBar() default constructor → GlassSegmentedControl
// BEFORE
GlassTabBar(
tabs: [GlassTab(label: 'A'), GlassTab(label: 'B')],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
)
// AFTER
GlassSegmentedControl(
segments: [GlassTab(label: 'A'), GlassTab(label: 'B')],
selectedIndex: _selectedIndex,
onSegmentSelected: (i) => setState(() => _selectedIndex = i),
)
GlassSegmentedControl.segments: List<String> → List<GlassTab>
// BEFORE
GlassSegmentedControl(segments: ['Daily', 'Weekly', 'Monthly'], ...)
// AFTER
GlassSegmentedControl(
segments: [
GlassTab(label: 'Daily'),
GlassTab(label: 'Weekly'),
GlassTab(label: 'Monthly'),
],
...
)
iOS 26 control vocabulary — full mapping #
| Widget | iOS 26 equivalent | Glass tier |
|---|---|---|
GlassSegmentedControl(...) |
UISegmentedControl |
Light tint + glass pill |
GlassSegmentedControl.scrollable(...) |
Scrollable filter tabs | Light tint + glass pill |
GlassTabBar.bottom(...) |
UITabBar |
Full liquid glass |
GlassTabBar.searchable(...) |
UITabBar + search |
Full liquid glass |
New: Configurable label colors and indicator border radius for bottom bars #
GlassTabBar.bottom(), GlassTabBar.searchable(), GlassBottomBar, and GlassSearchableBottomBar expose three additional styling parameters:
selectedLabelColor— tab label colour when selected, independent ofselectedIconColorunselectedLabelColor— tab label colour when unselected, independent ofunselectedIconColorindicatorBorderRadius— pill indicator corner radius, independent ofbarBorderRadius(e.g.100for a fully round pill on a subtly curved bar)
All three are optional; omitting them preserves existing behaviour exactly.
GlassTabBar.bottom(
tabs: [...],
selectedIndex: _selectedIndex,
onTabSelected: (i) => setState(() => _selectedIndex = i),
selectedLabelColor: Colors.blue,
unselectedLabelColor: Colors.grey,
indicatorBorderRadius: 100,
)
0.17.1 #
🐛 Fix — platformViewBackdrop toggle no longer snaps the selected-tab indicator (#112 by @jfhair) #
Toggling platformViewBackdrop at runtime (e.g. switching between a map tab and a Flutter-content tab) caused the selected-indicator pill to snap to the new tab instead of sliding. The spring animation controllers inside the indicator subtree were being re-seeded at their already-settled value because the toggle added or removed the LiquidGlassBlendGroup wrapper in AdaptiveLiquidGlassLayer, changing the child's depth in the element tree and forcing Flutter to discard and re-inflate the whole subtree via initState.
Fix: AdaptiveLiquidGlassLayer is now a StatefulWidget that holds a stable GlobalKey. The child is always wrapped in a KeyedSubtree with that key, so the element identity is preserved across the wrapper toggle — the indicator's AnimationControllers survive and the morph continues correctly.
No API changes. No breaking changes.
0.17.0 #
🔬 iOS 26 Concave Lens Pinch — All Four Pill Widgets #
The indicatorPinchStrength concave lens warp is now unified across all four interactive pill widgets. During a drag the pill edges curve inward (iOS 26 "through a lens" effect). Fully tunable — 0.0 disables it, 1.0 is maximum distortion.
New parameters #
GlassTabBar.indicatorPinchStrength(default0.4)GlassTabBar.indicatorExpansion(defaultEdgeInsets.symmetric(horizontal: 12, vertical: 8))GlassSegmentedControl.indicatorPinchStrength(default0.4)GlassSegmentedControl.indicatorExpansion(defaultEdgeInsets.symmetric(horizontal: 12, vertical: 8))AnimatedGlassIndicatorexported from the public API — enablesbaseIndicatorSettings.copyWith(...)from app code.
Changed defaults (AnimatedGlassIndicator.baseIndicatorSettings) #
glassColor:alpha: 0.15→alpha: 0.0— glass pill no longer applies a white tint overlay by default.chromaticAberration:GlassDefaults.chromaticAberration→0.15— the iridescent rim fringe is now explicitly set for iOS 26 parity.
Bug fixes #
GlassSegmentedControlrefraction — labels are now refracted through the glass pill atGlassQuality.premium(was rendered in wrong z-order).GlassTabBarindicator radius — resting pill now inherits the tab bar'sborderRadius(was hardcoded16 px).AnimatedGlassIndicatorsettings merge — partialindicatorSettingsoverrides no longer silently resetchromaticAberration.- Pinch lens jitter at rest — icon and label content no longer shimmers through the lens when the pill settles. Root cause: the jelly spring's micro-oscillations (±10 % of
thickness) were directly amplified into the UV warp. Fixed by applying a quadratic ease-out to the pinch multiplier (1 − (1 − fade)²), compressing the near-settled oscillation range ≈10×.
Try it — Indicator Parity demo #
The example app includes a live Indicator Parity demo (Demos → Indicator Parity) with all four pill widgets side-by-side and real-time sliders for pinchStrength, indicatorExpansion, and chromaticAberration. Use it to tune parameters before writing any code.
🌑 Apple Dimming Layer — LiquidGlassSettings.backerColor (#111 by @jfhair) #
New optional backerColor on LiquidGlassSettings — a shape-matched color pad composited behind the glass, giving a control's content contrast over rich or colorful backdrops (video, maps, photography) where the glass tint alone can't. This is Apple's "dimming layer" guidance from the Human Interface Guidelines (Materials section) and the pattern behind SwiftUI's clear Glass variant.
LiquidGlassSettings(
glassColor: Color(0x20FFFFFF),
backerColor: Color(0x59000000), // ~35% black — Apple's starting point
)
backerColor(Color?, defaultnull) — the color's alpha is the dimming opacity.nullmeans no backer, so all existing recipes are untouched.- Rendered at the widget level (like
shadow) and clipped to the glass shape viaClipRRect, so it composites correctly even over aPlatformView— maps, video — where a shader-side tint cannot reach. - Applies in both light and dark mode, and for flat-edge shapes (a bar over a map is a primary use case).
- Skipped on the grouped path (like shadow) — inserting a
Stackbetween grouped glass and its shared layer would break metaball morphing. lerpfadesbackerColorsmoothly from transparent when one side isnull, rather than snapping at the midpoint.
Migration #
All four widgets share the same tuning API:
indicatorPinchStrength: 0.4,
indicatorExpansion: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
indicatorSettings: AnimatedGlassIndicator.baseIndicatorSettings
.copyWith(chromaticAberration: 0.15),
0.16.3 #
✨ GlassTintBlend — selectable tint blending path (#107 by @jfhair) #
New GlassTintBlend enum on LiquidGlassSettings to explicitly control how glassColor blends with the refracted backdrop, instead of relying entirely on the chroma heuristic.
GlassTintBlend.auto— the default. Existing chroma gate, byte-for-byte unchanged behavior.GlassTintBlend.luminosity— always preserve backdrop luminosity. For near-neutral tints that need to keep the glassy look rather than flattening to a film.GlassTintBlend.flat— always impose the tint's brightness. For dimming layers, backing scrims, or deliberate frost-film surfaces.
Fully threaded through LiquidGlassSettings (copyWith, lerp, props), both the Premium and Standard shader paths, and preserved through AdaptiveGlass elevation rebuilds. Frosted fallback renders flat by construction and ignores the setting.
✨ GlassScrollEdgeEffect.bottomFadeInset (#109 by @jfhair) #
New optional bottomFadeInset parameter (default 0.0) that lifts the bottom fade off the widget's true bottom edge by the specified logical pixels. Fixes cases where the scroll viewport extends below the visible area — such as a bottom sheet whose content box overflows past the screen bottom — causing the bottom fade to anchor off-screen and never appear.
No breaking changes. All new parameters are optional with safe defaults.
0.16.2 #
🐛 Bug Fix — GlassMenu / GlassPopover rebuild on keyboard open/close #
GlassMenu and GlassPopover were rebuilding on every keyboard open/close event,
even when closed. Caused by MediaQuery.of(context) in didChangeDependencies
subscribing to viewInsets. Fixed by switching to scoped accessors
(disableAnimationsOf, maybeSizeOf, textScalerOf). Regression tests added.
✨ Content-luminance scroll-edge scrim (#106 by @jfhair) #
The continuous companion to the contentAwareBrightness discrete lever. Scroll-edge
fades now track content luminance and dissolve toward a dark color as dark content
scrolls under the bars — matching the native App Store early-darkening behaviour.
GlassContentAwareScope.register()gainsonLuminanceChanged(brightness callback is now optional). Per-rect mean luminance is delivered from the existing single capture; deliveries are gated on >0.005 movement.GlassScrollEdgeEffect.contentAwareFade— each edge band registers with the scope and lerps towarddarkFadeColoras content darkens (luminanceDarkBelow/luminanceLightAbovethresholds, 280 ms ease-out). Inert without a scope.GlassScaffold.contentAwareEdgeFade— one flag to enable both bars, composing withcontentAwareBrightness.- Latent wrap-order fix (0.16.0):
GlassScaffoldwas wrapping the body inGlassContentAwareContentafter the edge fade, so the fade overlays were inside the sampled region. With the adaptive scrim this is a feedback loop. Wrap order corrected + regression test added.
🐛 GlassModalSheet handle-drag fixes (#106) #
- Handle drag no longer fights the inner scroll.
_handleDragActivenotifier is set on pointer-down (before the innerScrollablecan claim slop), disabling inner scroll for the gesture lifetime. Fixes a freeze when dragging the handle over aPlatformView. dragIndicatorColornow actually reaches the drag indicator — it was silently wired into_SheetLayoutbut never forwarded to_GlassDragIndicator.
🐛 GlassEffect — defer capture when boundary is mid-paint (#106) #
toImageSync called during a dirty repaint boundary spammed [GlassEffect] toImageSync failed in debug and dropped the frame's capture. Guarded with debugNeedsPaint check (release-safe, same pattern as GlassScrollEdgeEffect).
No breaking changes. New parameters are all optional with safe defaults — existing code compiles and behaves identically without changes.
0.16.1 #
🍎 iOS 26 Indicator Defaults — Parity Calibration #
Three indicator defaults have been updated across GlassBottomBar and
GlassSearchableBottomBar to better match the iOS 26 bottom-bar pill
out of the box. No API changes — all parameters remain fully configurable.
Changed defaults #
indicatorPinchStrength — 1.0 → 0.4
The previous default of 1.0 applied the maximum concave lens / pinch effect
during drag. iOS 26's actual pinch is more restrained — 0.4 produces the
characteristic "through a lens" look without over-distorting the edges.
To restore the previous behaviour:
GlassBottomBar(
indicatorPinchStrength: 1.0,
...
)
indicatorExpansion — EdgeInsets.all(8) → EdgeInsets.symmetric(horizontal: 12, vertical: 8)
The indicator pill in iOS 26 bottom bars is slightly wider than it is tall — a subtle "landing pad" shape that reads as a rounded rectangle rather than a near-circle. The new default matches this proportion.
To restore the previous behaviour:
GlassBottomBar(
indicatorExpansion: const EdgeInsets.all(8.0),
...
)
AnimatedGlassIndicator chromatic aberration — 0.0 → 0.15
The indicator's internal _baseGlassSettings now sets
chromaticAberration: 0.15. Real iOS 26 glass has a faint iridescent
rainbow fringe at the rim. At 0.15 the effect is a whisper — visible
up close, subliminal during normal use.
To disable the aberration pass a full indicatorSettings override:
GlassBottomBar(
indicatorSettings: LiquidGlassSettings(
chromaticAberration: 0.0,
// include other fields you need
),
...
)
Affected widgets #
GlassBottomBar—indicatorPinchStrengthandindicatorExpansionGlassSearchableBottomBar—indicatorPinchStrengthandindicatorExpansion- All widgets using
AnimatedGlassIndicator—chromaticAberrationbaseline
GlassTabBar and GlassSegmentedControl retain their existing expansion
defaults (EdgeInsets.all(8.0)) as their geometry is different from a
bottom navigation bar.
0.16.0 #
🎨 Content-Aware Light/Dark Adaptation #
Glass bars now automatically adapt their icon and label colors to match the content scrolling behind them — light glyphs over dark content, dark glyphs over light content — with a smooth cross-fade transition. This matches the iOS 26 behaviour where navigation chrome remains legible regardless of what is visible underneath.
Core engine contributed by @jfhair in PR #103.
New widgets #
GlassContentAwareScope— wraps a screen and owns the sampling engine. Captures the content boundary at scroll rate (~5 fps), divides each registered control's rectangle into voting cells, and delivers per-control brightness verdicts via WCAG contrast ratios and dual-threshold hysteresis.GlassContentAwareContent— marks the sampled content region. Installs aRepaintBoundarythat the scope captures. Controls must be outside this region (e.g. inScaffold.bottomNavigationBarwithextendBody: true).GlassContentAwareBrightness— per-control consumer that cross-fades between the light and darkGlassThemeVariantviaGlassThemeVariant.lerp. Supports an externalbrightnessOverride(for PlatformView escape hatches), configurable grid dimensions, and per-control flip duration/curve overrides.
New parameters on existing widgets #
GlassBottomBar—adaptiveBrightness,brightnessOverride,onBrightnessChanged. SetadaptiveBrightness: trueto opt in.GlassSearchableBottomBar— same three parameters. Both bars automatically wrap inGlassContentAwareBrightnesswhen enabled.GlassScaffold—contentAwareBrightness. Whentrue, the scaffold automatically wraps the body inGlassContentAwareContentand the entire layout inGlassContentAwareScope. One flag, no manual widget wiring.
New API surface #
GlassThemeVariant.lerp(a, b, t)— interpolates settings, glow colors, quality, and border radius between two theme variants. Used internally by the cross-fade but available to consumers building custom transitions.GlassThemeSettings.lerp(a, b, t)— interpolates all 9 glass setting fields (thickness, blur, glassColor, lightAngle, lightIntensity, etc.).resolveBarLabelColor(context, brightness)— shared utility for bars to resolve label color fromCupertinoThemegiven an overridden brightness.
Usage #
The recommended path — one flag on GlassScaffold, one on the bar:
GlassScaffold(
contentAwareBrightness: true,
bottomBar: GlassBottomBar(
adaptiveBrightness: true,
onBrightnessChanged: (b) => /* flip your own icon colors */,
tabs: [...],
selectedIndex: _index,
onTabSelected: (i) => setState(() => _index = i),
),
body: CustomScrollView(...),
)
For custom layouts without GlassScaffold, use the standalone widgets directly:
GlassContentAwareScope(
child: Scaffold(
extendBody: true,
body: GlassContentAwareContent(
child: ListView(...),
),
bottomNavigationBar: GlassBottomBar(
adaptiveBrightness: true,
...
),
),
)
Bug fix #
GlassThemeVariant.==/hashCodemissingborderRadius— fixed a pre-existing bug where two variants differing only inborderRadiuscompared equal. This caused stale radius during content-aware cross-fades where intermediate lerped variants would not trigger rebuilds.
Polish #
_sample()error reporting — the barecatch (_)in the sampling pipeline now reports toFlutterErrorinside an assert closure. Errors are still suppressed in release builds but surface in debug mode so programming mistakes are visible.- Removed redundant
setState—_GlassContentAwareBrightnessState._setBrightnessno longer callssetStatesinceAnimatedBuilderalready listens to the animation controller and rebuilds on every tick.
Example app #
- Content-Aware Brightness demo (new) — dedicated showcase with alternating light and dark content bands that force visible bar flips during scrolling. Available in the Examples tab.
0.15.7 #
🌙 Adaptive Brightness Fix #
Fixed a bug in LightweightLiquidGlass where the shader's internal brightness estimation (backdropLuma) was incorrectly reading from the OS-level MediaQuery.platformBrightnessOf(context) rather than the inherited Flutter Theme.of(context).brightness.
This ensures that glass surfaces now correctly switch to Light Mode parameters (such as the legibility veil) when the app itself overrides the theme to Light Mode, even if the user's physical device remains in Dark Mode.
0.15.6 #
🌫️ Scroll Edge Fade — Perceptual Gradient Curve #
Replaced the 2-stop linear alpha gradient in GlassScrollEdgeEffect with a
multi-stop eased curve that matches the perceptual dissolve of iOS 26.
- 5-stop gradient profiles for both
softandhardstyles — eliminates the "denser in the centre" banding and the visible seam at the fade boundary. hardstyle reworked: steeper hold → sharper drop curve instead of just compressing the soft profile. Height multiplier relaxed from 0.33× to 0.5×.- Example app: Nav Patterns demo now fully brightness-aware (adaptive text
colours,
GlassStatusBarStyle.auto, adaptive solid-bar colour).
No API changes. No breaking changes.
0.15.5 #
✨ Whiten Strength — Light-Mode Legibility Veil #
Opt-in whitening ("legibility veil") lifts glass toward white for legibility over busy light backgrounds — modelling iOS 26's light-mode glass.
-
LiquidGlassSettings.whitenStrength(0.0–1.0, default 0.0): lifts the finished glass toward white as the last step of the render. A single control-wide value with no spatial seams or halo artifacts. -
LiquidGlassSettings.whitenGated(defaulttrue): when gated, the lift scales by per-pixel luminance so bright content lifts to white while dark content (text, icons) stays crisp. Ungated applies the lift uniformly — useful for dark-mode frost effects. -
Consistent across all three quality tiers from one knob: Premium (fragment shader), Standard (tint lerp), and Minimal (frosted fallback) all render the same whitenStrength value consistently.
-
GlassSearchableBottomBarwhiten-at-bottom: when ascrollControlleris provided, the bar animates its whitening toward full white as the page nears the scroll bottom — the iOS light-mode behaviour where content crowding under a bar gets the strongest legibility lift. -
Example app: Buttons & Shadows demo now includes a real-time whiten slider with side-by-side comparison cards and scroll-to-bottom boost preview.
Contributed by @jfhair in PR #100.
🎬 Scale-with-Morph — Cohesive Overlay Content Reveal #
Menu items and popover content now scale in alongside the liquid morph animation instead of popping in at the tail. The glass container and its content feel like a single continuous motion.
- Items enter the tree at 30% morph progress (down from 94%) and scale from
0.5× to 1.0× via an
easeOutcurve alongside opacity. Text and icons visually grow with the expanding glass container. - Applied to both
GlassMenuandGlassPopoverfor consistent overlay behaviour across the widget family. Content scales in on open and scales back out on close — the morph animation is symmetrical in both directions. - No new API surface. No breaking changes. Purely visual polish.
GlassMenu scale-with-morph contributed by @F1orian in PR #97.
0.15.4 #
⚡ Render Pipeline Performance Pass — FPS & Battery #
Internal optimizations targeting the render object paint() hot path and background
capture lifecycle. Zero API changes, zero visual changes. All 2,050 tests passing.
🎯 Bottom Bar Lateral Sway #
- Subtle lateral sway on fast pill drags —
GlassBottomBarandGlassSearchableBottomBarnow respond with a near-subliminal horizontal shift when the interactive pill is flicked quickly, matching iOS 26 bottom bar physics. Velocity-gated (slow drags produce no movement) and spring-animated back to center on release.
GPU allocation pressure (FPS) #
- Cached
ImageFilterin_RenderLightweightGlass— the composed blur+saturationImageFilter(and its 20-elementColorFilter.matrixlist) was previously reconstructed on everypaint()frame for every visible glass widget. With 5 glass cards at 60 fps, that's ~300 list allocations/second hitting the GC. The filter is now cached on the render object and only rebuilt whenblurorsaturationchanges. - Cached
ImageFilterin_RenderInteractiveIndicator— same optimization for the interactive indicator shader path (GlassEffect). The brightness+blur composed filter is now cached and invalidated only whenblurSigmachanges. - Cached
ImageFilter.blurinRenderLiquidGlassLayer— the premium glass layer'sBackdropFilterLayerblur filter was previously recreated on everypaintLiquidGlass()call, even when the blur sigma hadn't changed. During jelly/morph animations (60 fps), this creates anImageFilter.blurobject per frame per premium glass layer. Now cached and reused. - Cached
_shapesWithGeometrylist — the premium glasspaint()method previously allocated a new list on every frame to collect active geometry shapes. It now reuses a cleared instance list, eliminating another per-frame allocation on the hot path. - Cached light angle trigonometry — both
_RenderLightweightGlassand_RenderInteractiveIndicatornow cachecos(lightAngle)and-sin(lightAngle)instead of recomputing them on every_updateShaderUniforms()call. Matches the caching pattern already used by the premiumLiquidGlassRenderObject._cachedLightDir. - Conditional
alwaysNeedsCompositing— both_RenderLightweightGlassand_RenderInteractiveIndicatorpreviously returnedtrueunconditionally, forcing the framework to create a compositing layer even in the fallback path (null shader or zero blur). Now returnstrueonly when aBackdropFilterLayerwill actually be pushed. Reduces layer tree depth on low-end devices.
Battery life #
- Background Ticker auto-suspend —
LightweightLiquidGlassruns a Ticker to capture the background behind each glass widget viatoImageSync. Previously, this ticker ran at 60 fps permanently — even when the background hadn't changed for minutes (e.g. a static bottom bar over a static page). After 3 consecutive no-change frames (~50 ms), the ticker now self-suspends. It restarts automatically whendidUpdateWidgetdetects a configuration change. For a bottom bar with 5 tab pills, this eliminates 300 unnecessary method calls/second while idle.
GC pressure on frame timing path #
- Optimised
GlassQualityAdapter._percentile— the Phase 3 runtime percentile computation previously called_window.toList()+List.from(data)..sort()every 120 frames, creating two heap-allocated lists on the frame timing callback path. Now reuses a pre-allocated sort buffer (List<int>.filled) that is overwritten in-place. Eliminates GC pressure from the very code path that monitors for GC-induced frame drops.
Community contribution #
- Collapsed search indicator stretch feedback — the collapsed indicator in
GlassSearchableBottomBarnow wraps inLiquidStretch, giving it the same press-scale physics as the normal tab indicator. Contributed by @g3mf0r in PR #96.
Widget tree rebuild reduction #
- Shared spring listener in
GlassSearchableBottomBar— the three morph- animationAnimationControllers (tab width, search left, search width) each had their own anonymousaddListener(() => setState(() {}))callback. During a pill morph all three springs tick every frame, producing three independentsetStatecalls per frame. Now all three share a single named_onSpringTickcallback — Flutter coalesces the dirty mark so only one rebuild fires. Also fixes a subtle listener leak: anonymous lambdas can't be matched byremoveListener, but named methods can.
Consistency fix #
GlassEffect.preWarm()dummy image — changed from asyncawait picture.toImage(1, 1)to synchronouspicture.toImageSync(1, 1), matching theLightweightLiquidGlasspattern. Eliminates a 1-frame async initialization delay for a 1×1 transparent texture. Addedpicture.dispose()to prevent a minor leak.
0.15.3 #
platformViewBackdrop — Premium Glass Over iOS PlatformViews #
New opt-in flag that lets glass bars render correctly over native iOS views
(Google Maps, Apple Maps, WebView, MapLibre, video players) while keeping the
premium indicator animations alive. Previously, developers had to downgrade to
GlassQuality.standard on iOS — now they can stay on premium.
Contributed by @jfhair in PR #94.
New parameter: platformViewBackdrop #
GlassBottomBar— newplatformViewBackdropparameter. Whentrue, the bar background renders via liveBackdropFilter(the premium shader'stoImageSynccannot capture aUIKitView), while the premium indicator refracts the bar's own icon layer instead of the un-capturable backdrop.GlassSearchableBottomBar— same parameter, applied to both the tab indicator and the search pill. The collapsed search button automatically falls back toGlassQuality.standardover a PlatformView.AdaptiveGlass/AdaptiveGlass.grouped()— newplatformViewBackdropparameter that forces theBackdropFilterrendering path even atGlassQuality.premium.AdaptiveLiquidGlassLayer— newplatformViewBackdropparameter that skips theLiquidGlassBlendGroupwrapper when rendering over a PlatformView.
Usage #
GlassBottomBar(
quality: GlassQuality.premium,
platformViewBackdrop: Platform.isIOS, // ← one line fix
tabs: myTabs,
selectedIndex: _index,
onTabSelected: (i) => setState(() => _index = i),
)
Example app #
- Google Maps demo — updated to use
platformViewBackdrop: Platform.isIOSinstead of the oldPlatform.isIOS ? GlassQuality.standard : GlassQuality.premiumworkaround. The bar now stays atGlassQuality.premiumon all platforms.
0.15.2 #
GPU SDF Shadows — Light Mode Performance & Metaball Support #
Replaced the previous inverse-clip shadow system with GPU SDF shadows that render
directly from the merged geometry matte. This is both a performance improvement
and a visual correctness fix — shadows now correctly follow the liquid metaball
shape during morph animations, which was impossible with the old per-element
ClipPath approach.
Light mode shadows #
- GPU SDF shadow pass —
RenderLiquidGlassLayer.paintGlassnow includes a Pass 0 that draws eachBoxShadowusing the geometry matte image. The matte is drawn withImageFilter.blurfor the shadow spread, tinted viaColorFilter.mode, and then the interior is punched out withBlendMode.dstOutso the glass backdrop filter never samples its own shadow. This replaces the widget-level_InversePillClipper/_InverseOvalClipperClipPathapproach. - Shadows plumbed through the full stack —
LiquidGlassSettings.effectiveShadowis now resolved at theAdaptiveGlassandAdaptiveLiquidGlassLayerlevel and passed throughLiquidGlass.withOwnLayer→LiquidGlassLayer→_RawShapes→RenderLiquidGlassLayer. Dark mode and flat-edge shapes (borderRadius: 0) correctly receive an empty shadow list. - Removed old clip-based shadow system — deleted
_InversePillClipper,_InverseOvalClipperfromGlassBottomBar, and the equivalent clippers fromGlassSearchableBottomBar. Removed_wrapWithLightModeShadowfromAdaptiveGlass. This eliminates ~140 lines of widget-level shadow plumbing and the associatedClipPathcompositing cost. - Geometry image exposed to subclasses —
LiquidGlassRenderObjectnow exposesgeometryImageandgeometryLocalBoundsas@protectedgetters soRenderLiquidGlassLayercan access the matte for shadow rendering.
GlassMenu overlay rendering #
AdaptiveLiquidGlassLayerin menu overlay — the morph overlay now usesAdaptiveLiquidGlassLayerinstead of rawLiquidGlassLayer, ensuring correct quality resolution and backdrop blur when the menu is rendered in premium mode.
Example app #
- Buttons & Shadows demo — locked to Light Mode (
Brightness.light) to serve as the definitive showcase for GPU SDF shadows. Moved from the Demos tab to the Examples tab with updated card gradient and subtitle. - Apple Messages demo — menu trigger buttons now use
useOwnLayer: trueto correctly enter the GPU SDF shadow pipeline. Menu glass settings are brightness-aware (Colors.white12dark /Color(0x99FFFFFF)light). - Surfaces demo —
_buildDemoBackground()now acceptsBuildContextand returns a light frosted gradient in Light Mode instead of the hardcoded dark navy gradient that caused unreadable text.
Bug fixes #
- Fixed unused import
snap_rect_to_pixels.dartinliquid_glass_layer.dart. - Fixed unused
isDarkvariable inglass_menu_internal.dart. - Removed unnecessary
package:flutter/material.dartimport fromadaptive_liquid_glass_layer.dart(already provided bycupertino.dart). - Fixed duplicate doc comment in
glass_bottom_bar.dart. - Fixed release-mode crash in
GlassScrollEdgeEffect._captureBackground—RenderRepaintBoundary.toImage()throws synchronously onlayer!when called before the first paint completes (layer not yet composited). ThedebugNeedsPaintguard only runs insideassert()and is stripped in release builds. Fixed by deferring the initial capture to a post-frame callback and wrappingtoImage()intry/catchas a safety net. Falls back to the solid-colour gradient overlay on failure. (reported by @RuslanTsitser via #93) - Updated
GlassBottomBargolden test reference image.
0.15.1 #
Full Light & Dark Mode — Complete Adaptive UI Kit #
liquid_glass_widgets is now a fully adaptive iOS 26 UI kit. Every widget
— buttons, menus, search bars, sliders, switches, sheets, toasts, chips, form
fields, and all surface bars — automatically resolves the correct glass color,
rim lighting, shadow, text, and icon values for both Light and Dark
mode. No manual configuration required.
This release closes the last remaining light-mode rendering gaps and ships a reference implementation in the Apple Messages demo that matches the iOS 26 Messages app in both modes. Run the example app to toggle light and dark mode for all demo pages.
New features #
- Configurable glass shadow —
LiquidGlassSettings.shadowElevationscales the light-mode drop shadow (0.0= off,1.0= default,2.0= double).LiquidGlassSettings.shadowaccepts a customList<BoxShadow>for full control. Both flow throughglobalSettings(theme-level) and per-widgetsettings:. GlassShadowconstants — centralised shadow values (GlassShadow.elevation,.contact,.defaults,.scaled(double)) exported for custom widget authors.GlassButtonGroup.icons()— introduced a lightweight group constructor for iOS 26 style segmented icon toolbars. Uses a singleGlassButtonparent to drive cohesive group-level stretch/glow interaction, while children are rendered as zero-overhead stateless tap targets.GlassMenuController— imperative controller forGlassMenuwithopen(),close(), andisOpen. Drives the menu programmatically instead of (or in addition to) tapping the trigger — useful for gesture-arena-driven menus. (contributed by @F1orian)GlassMenu.showDismissBarrier— whenfalse, suppresses the full-screen tap-to-dismiss barrier so an external gesture owner can keep receiving pointer events while the menu is open. Defaults totrue. (contributed by @F1orian)GlassMenu.morphFromZero— whentrue, the menu body lerps from a zero-size point at the trigger center instead of from the trigger's own dimensions, suppressing the spawn blob (Blob A). For invisible or zero-sized triggers. (contributed by @F1orian)GlassMenuController.setFollowOffset(Offset)— nudges the open menu to track a moving anchor in real-time. The offset is added to the captured trigger position each frame and reset on the nextopen(). (contributed by @F1orian)
Light & dark mode improvements #
- Complete adaptive rendering — all widgets now resolve glass color, rim borders, shadows, text, and icon colors from
CupertinoTheme.brightnessOf(context). Switching between light and dark mode requires zero widget-level changes. - Light-mode rim borders — removed the heavy dark rim border on glass surfaces in light mode. Dark mode rim lighting remains fully intact and unchanged.
- Light-mode drop shadows — added inverse-clipped drop shadows to glass surfaces in light mode (cards, standalone buttons, bottom bars). Shadows render outside the glass boundary so the backdrop filter doesn't blur them. Note: morphing elements like the search pill do not have shadows to prevent animation artifacts.
- Standard glass white frost — standard quality glass in light mode now correctly renders as clean frosted white instead of muddy grey.
- Dynamic color resolution — improved internal text and icon styling to accurately resolve
CupertinoColorsagainst the active theme brightness. GlassSearchBarandGlassTextFielddefault colors now brightness-aware — default text, icon, and glow colors now resolve dynamically againstCupertinoTheme.brightnessOf(context)instead of being hardcoded to white. This ensures correct contrast in both light and dark mode.
Bug fixes #
- Fixed missing
} else {inlightweight_glass.fragthat caused PATH B (standard widgets) to run inside PATH A. - Fixed
GlassSheetsharp corners on macOS by decoupling top and bottom border radius. - Fixed
GlassMenuselection pill alignment and hit-test accuracy when system text scaler is active. - Fixed
GlassChipresolving with invisible white text and icons in light mode. - Fixed Apple Podcasts demo incorrectly forcing dark-mode glass colors in light mode.
- Fixed
GlassFormFieldlabel (Colors.white) and helper text (Color(0x99FFFFFF)) being invisible in light mode — now resolves fromCupertinoColors.label/.secondaryLabel - Fixed
GlassPickervalue text (Colors.white) and chevron icon (Colors.white70) being invisible in light mode — now resolves fromCupertinoColors.label/.secondaryLabel. - Fixed
GlassPasswordFieldlock and eye toggle icons (Colors.white70) being invisible in light mode — now resolves fromCupertinoColors.secondaryLabel. - Fixed
GlassActionSheetforcing dark card background (Colors.black @ 0.65), dividers, and pressed highlights regardless of brightness — now resolves to light frosted glass in light mode. - Fixed
GlassToastforcing dark pill (Colors.black @ 0.7) and white text regardless of brightness — background and text now resolve from brightness for correct contrast in both modes. - Removed unnecessary
import 'package:flutter/material.dart'fromGlassFormField,GlassPicker,GlassPasswordField, andGlassToast. - Fixed
GlassMenumorph size going negative during spring undershoot on small triggers —currentWidth/currentHeightnow clamped to>= 0to prevent debugBoxConstraintsassertion. (contributed by @F1orian) - Fixed
GlassMenusub-pixel Impeller crash — body container is replaced withSizedBox.shrink()when dimensions fall below 1.0 logical pixel, preventing 0-area matte rasterisation. (contributed by @F1orian) - Fixed
GlassIconButtonbypassing theme quality chain —qualitynow passesnullthrough toGlassButton.custom()soGlassThemeHelpers.resolveQualitycan resolve from ancestor layers, theme, then widget default. (contributed by @F1orian)
⚠️ Breaking — Removed frosted well overlay from GlassTextField #
GlassTextField no longer applies a hidden internal darkening overlay ("frosted well") on top of the glass surface. Previously, a DecoratedBox with Colors.black.withValues(alpha: 0.12) plus a top-edge gradient was composited inside the glass shape to simulate an iOS 26 recessed input tray. This has been removed entirely.
Why: The frosted well fought against user-specified glassColor, making text fields appear darker/muddier than buttons with identical settings. It also caused visible nested border artifacts when the overlay's BorderRadius.circular() didn't match the glass surface's LiquidRoundedSuperellipse shape — especially with useOwnLayer: true and padding: EdgeInsets.zero.
Design philosophy: GlassTextField is a hero surface (search bars, compose bars, app bar inputs) — not a form field nested inside glass cards. glassColor is now the single source of truth for appearance, consistent with every other glass widget.
Migration: If you relied on the frosted well for visual distinction inside a grouped layout, set a slightly darker glassColor on the text field explicitly. Most users will see cleaner, more predictable text fields with no action required.
Example app #
- Input demos — replaced
GlassCardwrappers around form fields with flatCupertinoColors.systemFillcontainers. Glass text fields now render as standalone hero surfaces (useOwnLayer: true) inside flat-colored form sections, demonstrating the correct pattern. Glass-in-glass nesting is an anti-pattern. - Apple Messages demo — fully adaptive light and dark mode implementation. Light mode uses the iOS system grouped background (
#F2F2F7), brightness-aware white glass tint, and dynamic layer separation to enable shadows. Dark mode retains liquid blending via the sharedAdaptiveLiquidGlassLayer. Press interactions usepersistPressOnDrag: trueand an elevatedambientBaseLightin light mode so the pressed state remains visible while holding and dragging off the button edge.
0.15.0 #
⚠️ Breaking — API Cleanup & Standardisation #
Removes Material-leaning widgets and thin wrappers that don't map to real
iOS 26 components. Standardises the public API for v1.0 readiness. This is a
breaking release — users on ^0.14.0 will not auto-upgrade.
Deleted widgets #
| Widget | Migration |
|---|---|
GlassPanel |
Replace with GlassCard(padding: EdgeInsets.all(24)) |
GlassWizard / GlassWizardStep |
No direct replacement — use GlassStepper for sequential steps |
GlassSideBar / GlassSideBarItem |
No direct replacement — a proper UISplitViewController-style sidebar is planned for post-1.0 |
GlassSnackBar |
Replace with GlassToast (identical API — GlassSnackBar was documented as an alias) |
glassSettings → settings rename #
The glassSettings parameter on 8 widgets has been renamed to settings for
consistency. The Glass prefix on the widget name already identifies the
settings type — glassSettings was redundant.
Affected widgets: GlassBottomBar, GlassSearchableBottomBar,
GlassSegmentedControl, GlassButtonGroup, GlassMenu, GlassPopover,
GlassToolbar, GlassPicker.
Migration: Find-and-replace glassSettings: → settings: in your code.
GlassSheet.show() API Cleanup #
Removed dead Material parameters from GlassSheet.show() that have no effect in the liquid glass rendering pipeline:
backgroundColorelevationshapeclipBehaviorconstraints
Migration: Remove these parameters from your GlassSheet.show() calls. Use settings to configure the glass visual appearance, and margin / padding / borderRadius to control the layout and shape.
🎨 Content Colour Audit — Adaptive Light/Dark Mode #
All hardcoded Colors.white / Colors.black in widget content layers replaced with CupertinoColors adaptive equivalents. Widgets now render correctly in both light and dark mode without requiring a MaterialApp ancestor.
Affected widgets: GlassDialog, GlassActionSheet, all interactive and surface widgets.
GlassPage is now fully safe to use inside a pure CupertinoApp — the Theme.of guard no longer throws when no Material ancestor is present.
🐛 Bug Fixes #
GlassSheet sharp corners (All Platforms) #
Fixed a bug where GlassSheet would render with completely sharp, square corners on all devices (including iOS and Android). The internal SafeArea wrapper was consuming the bottom safe area before the adaptive radius calculation could read it, causing it to incorrectly fallback to 0.0 everywhere. GlassSheet now decouples its top and bottom border radii, defaulting to topBorderRadius: 32.0, and smartly assigning bottomBorderRadius to 32.0 when floating (with margin) or 0.0 when docked.
✨ New — GlassButtonStyle.prominent #
A new button style matching iOS 26's .prominentGlass / .glassProminent
configuration — thicker, more opaque glass for primary call-to-action buttons.
GlassButton(
style: GlassButtonStyle.prominent,
icon: Icon(CupertinoIcons.plus),
onTap: () {},
)
✨ New — GlassGroupedSection #
A convenience wrapper that groups GlassListTiles inside a GlassCard,
automatically applying isLast: true to the final tile to suppress its
bottom divider — the most common source of bugs.
GlassGroupedSection(
header: Text('Network'),
children: [
GlassListTile(title: Text('Wi-Fi')),
GlassListTile(title: Text('Bluetooth')),
GlassListTile(title: Text('VPN')), // isLast applied automatically
],
)
✨ New — GlassPageControl #
iOS UIPageControl equivalent — dot indicators for paged content (carousels,
onboarding, gallery pages). Features animated capsule-shaped active dot with
glass treatment and optional tap-to-navigate.
GlassPageControl(
count: 5,
currentPage: _currentPage,
onPageChanged: (page) => _pageController.animateToPage(page,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
),
)
📖 README — Glass vs Content design guide #
Added a design philosophy section explaining iOS 26's glass hierarchy:
glass for navigation chrome and controls, opaque for content areas.
Repositioned GlassContainer as an advanced primitive in the widget catalogue.
What stays #
GlassBackdropScope— deprecated no-op stays until 1.0.0 as previously committed in the 0.14.0 changelog. Zero cost to keep.- All other widgets — no changes.
🐛 Fix — GlassMenu auto-scrolling with large system text #
When the user increased system text size, GlassMenu would become scrollable
even without menuHeight being set. The height calculation now accounts for
textScaler so the menu sizes correctly at any accessibility text scale.
🐛 Fix — GlassMenu selection pill misalignment at large text scale #
The sliding selection highlight and hit-test math used the nominal 44px item
height for positioning, while the actual GlassMenuItem widgets grew taller
via ConstrainedBox when system text was scaled up. This caused the pill to
drift out of alignment and sometimes produce a "double highlight" effect. All
layout math (_getItemOffset, _calculateIndexFromPosition, _isScrollable)
now uses the TextScaler-aware height calculation.
🐛 Fix — GlassMenuItem / GlassMenuDivider / GlassMenuLabel Light Mode #
These widgets previously hardcoded Colors.white for text, icons, and divider
colours, making them invisible on light backgrounds. They now inherit from
CupertinoTheme.of(context):
| Widget | Colour Source |
|---|---|
GlassMenuItem |
theme.textTheme.textStyle.color → CupertinoColors.label |
GlassMenuDivider |
theme.textTheme.tabLabelTextStyle.color at 15 % opacity |
GlassMenuLabel |
theme.textTheme.tabLabelTextStyle.color at 45 % opacity |
Custom iconColor, titleStyle, and color parameters still take priority
over the theme default — zero breaking changes.
🧹 Core — CupertinoApp compatibility #
Removed all Theme.of(context) dependencies from the core library, replacing them with CupertinoTheme and defaultTargetPlatform. This ensures glass widgets adapt correctly to dark mode and background colours in pure CupertinoApp structures without falling back to Material defaults.
🧹 Example app — CupertinoApp migration #
All 16 standalone demos and the main showcase app have been migrated from
MaterialApp to CupertinoApp. This is the correct root widget for an iOS 26
glass library — it provides CupertinoTheme to the entire widget tree, which
glass widgets depend on for colour resolution.
A Theme(data: ThemeData.dark(...)) builder is injected below CupertinoApp
so that any Scaffold widgets in demo pages continue to receive proper Material
theming (background colour, text defaults).
🧹 Example app — GlassMenu demo controls #
Added text-scale slider (1.0×–3.0×) and light/dark theme toggle to the menu
demo. Also added GlassMenuLabel, GlassMenuDivider, and a destructive
GlassMenuItem to the menu items so their theme colour inheritance can be
visually verified.
0.14.2 #
🧹 Material Artifact Purge #
Replaced internal Material primitives with Cupertino-native equivalents for better iOS 26 fidelity. No public API changes.
- Tap feedback:
InkWell→GestureDetector+ iOS-style opacity highlight inGlassListTileandGlassActionSheet. - Icons:
Icons.chevron_right,Icons.add,Icons.remove,Icons.info_outline→CupertinoIconsequivalents inGlassListTileandGlassStepper. - Colours: Hardcoded
Colors.green/Colors.red→CupertinoColors.systemGreen/CupertinoColors.destructiveRedacrossGlassSwitch,GlassBadge,GlassWizard,GlassDialog,GlassMenuItem, andGlassFormField.
0.14.1 #
🐛 Fixed — GlassScaffold black background #
GlassScaffold with no background: widget was forcing the inner Scaffold to
Colors.transparent, rendering black instead of Theme.scaffoldBackgroundColor.
Added a backgroundColor: Color? parameter; the scaffold now defaults to the
theme colour when no explicit background is provided.
✨ Improved — Cancel icon size & customisation #
The dismiss × button in GlassSearchableBottomBar was hardcoded to size: 16,
which felt undersized compared to iOS 26. Default bumped to 24. Both
GlassSearchBar and GlassSearchBarConfig now expose cancelIconSize and
cancelIcon so the icon size and glyph are fully overridable.
✨ New — GlassPopover #
A new overlay widget that presents custom content in a glass container with
the same liquid morph animation as GlassMenu. Use it for tooltips, mini forms,
preview cards, colour pickers, or any content that should appear in a glass
popover — without being limited to a list of menu items.
GlassPopover shares GlassMenu's liquid teardrop expansion, spring physics,
auto-positioning, and screen-edge clamping. The contentBuilder receives a
close callback so popover content can dismiss itself.
GlassPopover(
trigger: GlassIconButton(
icon: Icon(CupertinoIcons.info_circle),
onPressed: null, // handled by GlassPopover
),
contentBuilder: (context, close) => Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Profile Details',
style: TextStyle(color: Colors.white, fontSize: 16)),
SizedBox(height: 12),
GlassButton(onTap: close, child: Text('Done')),
],
),
),
)
Key parameters:
contentBuilder— builder for custom popover content, receives aclosecallbacktrigger/triggerBuilder— the widget that opens the popover on tappopoverWidth/popoverHeight— size control (height defaults to intrinsic)alignment— manualGlassMenuAlignment, ornullfor auto-detectautoAdjustToScreen— screen-edge clamping (on by default, with 12px padding)barrierDismissible— whether tapping outside closes the popover (default: true)onOpen/onClose— lifecycle callbacks- Full
LiquidStretchandGlassGlowsupport
0.14.0 #
⚠️ Breaking — GlassAppBar redesigned as pure layout widget #
GlassAppBar has been fundamentally redesigned from a StatefulWidget with its own glass rendering to a lightweight StatelessWidget that serves purely as a layout container. This matches iOS 26's actual navigation bar pattern where the bar itself is transparent and glass effects belong on individual interactive elements.
Removed parameters:
settings— glass surface settings (the bar no longer renders glass)quality— glass quality selectoruseOwnLayer— layer isolation controlscrollController— scroll-driven glass transitionscrollEdgeThreshold— scroll fade threshold
New parameter:
buttonSettings: LiquidGlassSettings?— provides default glass settings to all descendantGlassButtonwidgets viaDefaultButtonSettings(a newInheritedWidget). Individual buttons can still override with their ownsettings.
Migration:
// Before (0.13.x) — scroll-driven glass bar:
GlassAppBar(
title: Text('Title'),
scrollController: _ctrl,
scrollEdgeThreshold: 50.0,
settings: LiquidGlassSettings(blur: 15),
quality: GlassQuality.premium,
)
// After (0.14.0) — transparent layout bar:
GlassAppBar(
title: Text('Title'),
buttonSettings: LiquidGlassSettings(
glassColor: Color(0x33FFFFFF),
thickness: 20,
),
)
// Use GlassScaffold.header + headerScrollController for fading headers.
Apps that relied on GlassAppBar.scrollController for scroll-driven glass should migrate to GlassScaffold with its header / headerScrollController parameters (see below).
✨ New — GlassScaffold — one-widget page layout #
A brand new scaffold that replaces the manual assembly of GlassPage + Scaffold + GlassScrollEdgeEffect + Stack with a single widget. Handles z-ordering (bars always above body), edge fading, safe-area padding, and glass layer setup automatically.
header + headerScrollController + headerFadeDistance #
A fixed header widget positioned below the status bar that fades out as the user scrolls — matching the iOS 26 large-title pattern used by Apple Music ("Listen Now"), Apple Podcasts, and similar apps.
GlassScaffold(
header: Text('Listen Now', style: largeTitle),
headerScrollController: _scrollController,
headerFadeDistance: 60.0,
body: CustomScrollView(
controller: _scrollController,
slivers: [...],
),
)
The header is wrapped in IgnorePointer only when fully faded (opacity == 0), so tappable elements remain interactive at full visibility.
bodyOverlays #
Optional list of widgets rendered between the body and the navigation bars in z-order. Designed for floating elements like Apple Music's play-bar pill that need to render above scrollable content but below the bottom bar.
GlassScaffold(
bodyOverlays: [
AnimatedPositioned(
bottom: isMiniMode ? miniBottom : aboveBarBottom,
left: 20, right: 20,
child: PlayBarPill(),
),
],
body: scrollContent,
)
AnnotatedRegion status bar fix #
GlassScaffold now wraps the internal Scaffold in an AnnotatedRegion<SystemUiOverlayStyle> so that the statusBarStyle setting correctly overrides CupertinoPageRoute's own region. Previously, pages without a GlassAppBar could lose their status bar icon styling when pushed via CupertinoPageRoute.
Improved isolation strategy #
GlassScaffold now wraps app bar and bottom bar in GlassIsolationScope(isolated: false, defaultQuality: GlassQuality.premium) instead of the previous GlassIsolationScope(isolated: true). This means bar buttons join the page-level glass blend group (shared backdrop capture) rather than creating their own layers — saving GPU cost. Stack paint order already guarantees bars render on top of body content.
Stable ValueKeys on stack children #
All conditional children (header, app bar, bottom bar) now have explicit ValueKeys so Flutter tracks them by identity rather than position. This prevents unmount/remount cycles (and loss of animation state) when toggling the header on and off.
See
example/lib/demos/nav_bar_patterns_demo.dartfor completeGlassScaffoldusage patterns.
✨ New — GlassIsolationScope.defaultQuality #
GlassIsolationScope gains a defaultQuality: GlassQuality? parameter that provides a quality hint for descendants without requiring explicit quality: on each widget. This is separate from isolated — a scope can be de-isolated (for grouping) while still providing a premium quality default.
GlassThemeHelpers.resolveQuality now checks scopeDefault and skips inherited page-level quality when a scope provides a defaultQuality. This fixes a regression where bar buttons inside GlassScaffold would inherit the page's standard quality instead of getting the bar's intended premium.
✨ New — DefaultButtonSettings InheritedWidget #
A new InheritedWidget in glass_app_bar.dart that provides default LiquidGlassSettings to descendant GlassButton widgets. GlassButton now checks DefaultButtonSettings.of(context) as a fallback between explicit settings and inherited layer settings.
✨ New — AdaptiveGlass interactive auto-promotion #
AdaptiveGlass now automatically promotes interactive elements (isInteractive: true) to their own compositing layer in premium mode. This gives buttons the prominent independent refraction that matches iOS 26's button design, rather than softly blending them into the page blend group. The own-layer wrapper de-isolates children via GlassIsolationScope(isolated: false) so nested glass (e.g. tab items inside a bottom bar) groups correctly with the button's layer.
🐛 Fix — GlassBottomBar extra button z-order #
Fixed a compositing z-order issue where the jelly tab indicator would incorrectly render behind the extra trailing button. The internal layout has been restructured from a Row to a Stack with explicit paint ordering — the extra button is painted first (bottom of z-order), and the tab indicator is painted last (top). This matches the existing pattern in GlassSearchableBottomBar.
🐛 Fix — GlassSearchableBottomBar pill z-order #
Fixed the paint order of the search pill and tab pill in GlassSearchableBottomBar. The search pill is now painted first (bottom of stack) and the tab pill last (top), so the glass indicator correctly renders on top when it overlaps the search pill during tab transitions.
🐛 Fix — mounted guards in gesture handlers #
Added if (mounted) guards before every setState() call in TabDragGestureMixin, TabIndicator, and SearchableTabIndicator. Pointer events (onPointerDown, onPointerUp, onPointerCancel) and drag gesture callbacks can fire after the widget has been unmounted during rapid tab switching or page transitions, causing setState() called on disposed widget exceptions in debug mode. These guards are zero-cost safety nets.
🔄 Changed — Nav bar patterns demo #
Removed three GlassAppBar demo patterns that used the now-deleted scroll-driven glass API:
Scroll-Driven Glass(transparent → glass on scroll)Static Glass(always-on glass surface)Large Title Glass on Scroll(combined collapsing + glass materialisation)
Added a new "Fade Header (No App Bar)" pattern demonstrating the GlassScaffold.header fade approach (Apple Music / Podcasts style).
🔄 Changed — Example app restructured #
Showcase app uses GlassScaffold #
The main showcase app (main.dart) now uses GlassScaffold instead of manually composing GlassPage + Scaffold + bottomNavigationBar. Added a fourth tab ("Examples") and redesigned the Widgets tab with a 2-column card grid.
Apple demos migrated to GlassScaffold #
All four Apple demo apps have been migrated from manual Scaffold + Stack + GlassScrollEdgeEffect composition to GlassScaffold:
- Apple Messages —
GlassScaffoldhandles positioning, z-ordering, and edge fading automatically. 8 lines changed. - Apple Music — uses
GlassScaffold.headerfor the "Listen Now" fade header,bodyOverlaysfor the floating play pill. Major simplification (294 lines changed). - Apple News — migrated to
GlassScaffold. Removed unnecessary nestedStack. 57 lines changed. - Apple Podcasts — uses
GlassScaffold.bodyOverlaysfor the mini-player pill. 372 lines changed.
Category pages re-indented #
All 6 showcase category pages (containers, feedback, input, interactive, overlays, surfaces) have been re-indented for consistency. No functional changes.
🐛 Fix — Impeller backdrop visual corruption #
Fixed a critical rendering issue where GlassAppBar buttons would lose their glass background (rendering as invisible) when multiple LiquidGlassLayers shared a single root BackdropGroup (via GlassBackdropScope). On Impeller, sharing a BackdropKey across multiple RepaintBoundary layers caused the engine to bind stale or incorrect backdrop textures.
Root cause: The shared BackdropGroup architecture assumed that all glass surfaces could safely share a single GPU backdrop capture. On Impeller's tile-based renderer, this created a race between layers trying to use the same BackdropKey, causing visual corruption (backgrounds vanishing, ghost artifacts during route transitions).
Fix: Each LiquidGlassLayer now creates its own isolated BackdropGroup → RepaintBoundary subtree. This ensures every glass surface captures only its own clipped bounding box — a ~30× reduction in GPU bandwidth compared to the previous full-screen capture, and eliminates cross-layer texture conflicts entirely.
⚠️ Deprecated — GlassBackdropScope #
GlassBackdropScope is now a no-op and can be safely removed from your widget tree. Each glass layer manages its own backdrop isolation automatically. GlassPage continues to work exactly as before — no migration needed for apps using GlassPage.
Apps using GlassBackdropScope directly will see a deprecation warning but will compile and run without issues. Remove the widget at your convenience; it will be deleted in 1.0.0.
🔄 Changed — GlassInteractionSettings is now the canonical interaction API #
GlassInteractionSettings (introduced in 0.13.0) is now the recommended way to configure all interaction physics app-wide. Pass it via GlassThemeData.interaction inside LiquidGlassWidgets.wrap():
runApp(LiquidGlassWidgets.wrap(
child: const MyApp(),
theme: GlassThemeData(
interaction: GlassInteractionSettings(
stretch: 0.2, // subtler drag-following globally
interactionScale: 1.03, // less scale-up on press
resistance: 0.01, // drag damping factor
anchorStretch: true, // iOS 26 rubber-band from anchor
anchorStretchSettings: AnchorStretchSettings(
intensity: 0.8,
squashFactor: 0.15,
),
),
),
));
Set stretch: 0.0 to disable drag-following app-wide while keeping press-scale. Individual widgets can still override any value via their own stretch: / interactionScale: parameters.
0.13.0 #
✨ New — Anchor Stretch, Ambient Light, and Interaction Physics #
AnchorStretchSettings — fine-tuned stretch feel #
A new configuration class for LiquidStretch that controls how widgets deform when pressed and dragged. Widgets now stretch from their anchor point toward the drag direction — matching iOS 26 button physics where the surface rubber-bands from its resting position rather than free-following the finger.
GlassButton(
anchorStretchSettings: AnchorStretchSettings(
intensity: 0.8, // more stretchy
squashFactor: 0.3, // perpendicular compression
translationDamping: 0.15, // center-shift toward finger
bounciness: 0.2, // elastic snap-back overshoot
),
)
All parameters have sensible defaults matching iOS 26 behaviour. Most developers won't need to change them.
GlassButton.ambientBaseLight — surface luminosity #
A subtle white overlay (default 0.08) applied during press/drag interactions. Simulates iOS 26 surface luminosity — when the directional glow tracks off-edge, the button still maintains a faint lit appearance rather than going completely dark.
GlassButton.persistPressOnDrag #
Controls whether the pressed visual state persists when the user's finger drags outside the button bounds. Defaults to true, matching iOS 26 behaviour where buttons stay visually pressed during drag-off and only release on pointer-up. Set to false for the traditional behaviour where leaving the hit-test area cancels the press.
GlassPage.settings — page-level glass configuration #
GlassPage now accepts an optional settings: LiquidGlassSettings parameter and internally wraps its child in an AdaptiveLiquidGlassLayer. This means all glass widgets inside the page (GlassAppBar, GlassCard, GlassButton, etc.) automatically inherit the page's glass settings — no need to set useOwnLayer: true or pass settings: to each widget individually.
GlassPage(
settings: LiquidGlassSettings(
glassColor: Color.fromRGBO(28, 28, 30, 0.8),
thickness: 30,
blur: 4,
),
child: Scaffold(...),
)
When settings is null, the layer inherits from GlassTheme or uses defaults.
GlassScaffold — comprehensive structural layer #
A new structural widget that coordinates interactions between GlassAppBar, GlassBottomBar, and the page body. It automatically handles edge-to-edge layout, scroll bleeding, and isolates glass rendering layers for maximum performance. This replaces the need to manually compose AdaptiveLiquidGlassLayer and GlassIsolationScope at the page root.
GlassInteractionSettings Theme #
Added a dedicated theme extension to globally configure interaction physics across all glass widgets — stretch, press scale, drag resistance, anchor stretch, and anchor stretch fine-tuning. No more passing interaction parameters to every widget manually.
GlassThemeData(
interaction: GlassInteractionSettings(
stretch: 0.2, // subtler drag-following globally
interactionScale: 1.03, // less scale-up on press
resistance: 0.01, // drag damping factor
anchorStretch: true, // iOS 26 rubber-band from anchor
anchorStretchSettings: AnchorStretchSettings(
intensity: 0.8,
squashFactor: 0.15,
),
),
)
Set stretch: 0.0 to disable drag-following app-wide while keeping press-scale. Individual widgets can still override any value.
🐛 Fixes & Improvements #
Wide Button Stretch Distortion #
Fixed a rendering artifact where very wide GlassButtons would exhibit severe shape distortion at the extreme edges during horizontal stretch physics.ts.
GlassSearchBarConfig.cursorColor — cursor follows Flutter theme by default #
Thanks to @jfhair for PR #71. 🙏
GlassSearchableBottomBar's expanded search field now exposes a cursorColor knob via GlassSearchBarConfig, and the default behaviour aligns with Flutter convention — the cursor follows the standard theme-resolution chain (Theme.of(context).textSelectionTheme.cursorColor → CupertinoTheme.primaryColor on iOS → Theme.of(context).colorScheme.primary) rather than being hard-coupled to textColor.
Apps that want the previous behaviour — cursor matching textColor — can opt in explicitly:
GlassSearchBarConfig(
textColor: Colors.white,
cursorColor: Colors.white, // ← previously implicit
)
⚠ Breaking change. Apps that set a
textColorand rely on the cursor implicitly matching it will see their cursor colour change to whatever theirTheme.of(context).colorScheme.primaryis (typicallyColors.blueif untouched). Two-line migration above.
🐛 Fix — Premium stretch edge clipping #
Premium-quality GlassButton could exhibit jagged rasterization edges during stretch deformation. The Impeller LiquidGlassLayer rasterizes at its native resolution, which doesn't perfectly align with the deformed shape boundary during stretch.
Fixed by wrapping the premium glass surface in a vector ClipPath at the shape boundary. This renders at screen resolution every frame while preserving full refraction, chromatic aberration, and 3D specular — no quality downgrade needed.
🐛 Fix — Mali GPU crash guard #
render_liquid_glass_geometry.dart now guards against zero and negative dimensions in both render() and renderAsync(). During jelly animations or rapid layout transitions (modal expansion, tab switching), matteBounds can momentarily collapse to zero dimensions — producing an invalid GPU texture request that crashes Mali drivers.
The fix returns a minimal 1×1 fallback cache for zero-dimension frames. Additionally, matte.toImageSync() is wrapped in a try/catch to handle Mali driver failures gracefully — returning a safe fallback instead of crashing the app. The next paint frame rebuilds the geometry with valid dimensions automatically.
🐛 Fix — Searchable bottom bar collapsed shape #
The collapsed search button and tab indicator in GlassSearchableBottomBar used LiquidRoundedSuperellipse even when collapsed to a square. A superellipse with borderRadius: 32 on a 50×50 square has subtle flat segments between the arcs — invisible at rest but clearly distorted during stretch deformation.
Fixed by switching to LiquidOval when constraints are square (within 2px tolerance), which renders a mathematically perfect circle that stretches uniformly. During the collapse animation (when the width is still wider than the height), the superellipse is used to avoid a squashed oval appearance.
🎨 Visual — iOS 26 thin glass defaults #
The three default theme variants have been standardised to match the thin, refractive glass aesthetic of iOS 26:
| Property | Dark | Light | Minimal |
|---|---|---|---|
| thickness | 40 → 10 | 20 → 12 | 30 → 10 |
| blur | 5 → 4 | 6 → 5 | 12 → 8 |
| lightIntensity | 1.5 → 0.7 | 1.2 → 0.85 | unchanged |
| lightAngle | unset → 135° | unset → 135° | unchanged |
| chromaticAberration | unset → 0.01 | 0.3 → 0.02 | unchanged |
⚠ Visual change. Apps using
GlassThemeVariant.dark,.light, or.minimalwithout explicitLiquidGlassSettingsoverrides will see thinner, subtler glass. This is intentional — the previous defaults were heavier than the native iOS 26 aesthetic. If you prefer the heavier look, set explicitthicknessandblurvalues in yourGlassThemeData.
⚠️ Semi-Breaking — GlassAppBar transparent by default #
GlassAppBar now renders a transparent navigation bar by default — no glass surface, no specular rim. This matches iOS 26's actual navigation bar pattern where the glass effect is on individual buttons, not the bar itself.
Previously, GlassAppBar always wrapped its content in an AdaptiveGlass surface with LiquidGlassSettings(blur: 15). This created a visible glass rectangle behind the title and actions — a Material-style app bar with glass paint, not an iOS 26 navigation bar. A better version of this to come next
To opt in to a glass background (e.g. for scroll-edge transitions), pass explicit settings:
// Before (0.12.x) — glass was always on:
GlassAppBar(title: Text('Title'))
// After (0.13.0) — transparent by default:
GlassAppBar(title: Text('Title')) // transparent, iOS 26 style
// Opt-in glass background:
GlassAppBar(
title: Text('Title'),
settings: LiquidGlassSettings(blur: 15, thickness: 10),
)
Additionally, quality now defaults to null (inherits from ambient scope) instead of GlassQuality.premium.
🎨 Visual — Specular rim refinement #
Standard/minimal quality glass surfaces now render a more refined specular inner-border rim:
- True inner border — the specular stroke is now clipped to its inner half via
_ShapeClip, creating an optically correct glass-edge reflection instead of a center-straddling stroke that bleeds outside the shape boundary. - Organic overlay blending —
BlendMode.overlayreplacesBlendMode.srcOver, so the rim reacts to the background colour underneath rather than appearing as a flat white line. Darker backgrounds produce subtler rims; lighter backgrounds produce brighter ones. - Flat-edge suppression — shapes with
borderRadius: 0(used byGlassAppBar,GlassSideBar,GlassToolbar) no longer render the specular rim. On full-width flat surfaces, the rim looked like a Material divider line rather than an internal glass reflection.
Only affects standard and minimal quality. Premium quality uses Impeller's native LiquidGlassLayer which has its own refraction-based edge rendering.
⚡ Performance — Quality adapter tuning #
- Faster degradation — Phase 3 runtime monitoring now triggers a quality step-down after 2 consecutive over-budget windows (previously 3), reducing reaction time from ~6 seconds to ~4 seconds. This means devices that genuinely can't sustain their assigned quality level are protected sooner.
- Documentation updated — removed provisional calibration warnings from warmup threshold docs. The 20ms premium / 28ms standard thresholds are now considered validated.
⚠️ Semi-Breaking — LiquidStretch.resistance default #
The default resistance value for LiquidStretch has changed from 0.08 to 0.01. This makes all stretch interactions feel more responsive and fluid — closer to the iOS 26 native feel. The previous value was overly dampened.
All widgets using LiquidStretch without explicitly setting resistance (including GlassButton, GlassCard, GlassContainer, GlassMenu) will feel stretchier. To restore the previous behaviour:
LiquidStretch(
resistance: 0.08, // previous default
child: ...,
)
🧪 Tests — 1898 passing (+124 new) #
- New
GlassButtontests:persistPressOnDragtrue/false behaviour, default values, cancel paths. - New
GlassSearchBarConfig.cursorColortests: default null, explicit value, independence fromtextColor. - Updated
glass_quality_adaptertests fordegradeWindowCount: 2. - Updated stretch tests for new
resistancedefault. - Updated
GlassAppBardefaults test for transparent-by-default change. - Updated golden tests for specular rim flat-edge suppression.
📦 Example app #
- Keypad lock screen demo — new full-screen demo showcasing
GlassButtonin a PIN-entry layout. - Restructured showcase pages — all category pages (containers, feedback, input, interactive, overlays, surfaces) reorganised for cleaner presentation. More work to come...
0.12.8 #
🐛 Fix — GlassTextField reverted to v0.12.4 + icon drift fix #
- Reverted to v0.12.4 — restored exact line-count and layout logic. The v0.12.6–0.12.7 changes introduced regressions (line breaks at wrong character boundary, icons pinned to container bottom).
- Fixed icon drift under system text scaling — in fixed-height mode, icons are now always centred relative to the container rather than relative to the text row. This prevents icons from shifting position when users change system text scaling. In dynamic-height mode (no
heightparameter),iconAlignmentis respected as before.
Thanks @g3mf0r for the detailed testing.
0.12.7 #
🐛 Fix — GlassTextField icon alignment (retained) + line-count regression fix #
iconAlignment: .endno longer drifts under system Large Text. TheCenterwidget wraps only theTextField, not the entire iconRow, soCrossAxisAlignment.end/.startworks correctly against the full container height. (retained from 0.12.6)- Reverted line-count measurement back to
renderBox.size.width(the v0.12.4 approach). The v0.12.6RenderEditablewidth change caused line breaks to fire a couple of characters early. Thanks @g3mf0r for catching this.
0.12.6 #
🐛 Fix — GlassTextField icon alignment and line-count accuracy #
iconAlignment: .endno longer drifts under system Large Text. TheCenterwidget now wraps only theTextField, not the entire iconRow, soCrossAxisAlignment.end/.startworks correctly against the full container height.- Line-count measurement is now pixel-perfect.
_measureLineCountwalks the render tree to find the actualRenderEditableand uses its layout width (which accounts for the internal_caretMargin≈ 3 px). Falls back gracefully if the render walk fails.
0.12.5 #
✨ New — GlassMenu.onClose callback #
Added onClose: VoidCallback? to GlassMenu. Fires when a close is triggered
(barrier tap, trigger re-tap, or item selection), before the animation completes.
Useful for synchronising external state such as a GlassMorphController.
Thanks to @g3mf0r for the contribution (#67).
0.12.4 #
🐛 Fix — GlassTextField layout and reactivity #
onLineCountChanged fires correctly under fixed-height constraints #
The onLineCountChanged callback silently stopped firing after the first
measurement when the field was inside a fixed-height container (e.g.
SizedBox(height: 46) or height: 46 on the field itself). The internal
guard used size == _lastTextFieldSize — but a fixed outer height keeps the
RenderBox size constant, so the guard always exited early after the first
call. The fix replaces the size-equality guard with a (text, constrainedWidth)
guard: the callback fires whenever the text content or available wrapping width
changes, regardless of what the outer height is doing.
This also resolves the stale-state reactivity bug where _lines stored in
State stopped updating borderRadius after re-opening the keyboard.
Placeholder and text stay vertically centred under system Large Text #
When height is specified (fixed-height mode), the outer padding's vertical
component was applied inside the SizedBox, pushing placeholder text and icons
downward when the user enabled a large system font. The field now strips
vertical padding in fixed-height mode and centres the text row via Align,
matching the behaviour of GlassSearchBar. The padding parameter's
horizontal values are unchanged.
✨ New — bottom panel for GlassTextField and GlassTextArea #
Both widgets now accept an optional bottom widget that renders below the text
area inside the same glass card. Use it to build the "rich composer" pattern — a
text input on top with an action bar, attachment strip, or formatting toolbar
below, all sharing one glass surface:
GlassTextField(
maxLines: 5,
minHeight: 44,
maxHeight: 160,
bottom: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
children: [
IconButton(icon: Icon(Icons.attach_file), onPressed: _attach),
const Spacer(),
IconButton(icon: Icon(Icons.send), onPressed: _send),
],
),
),
)
The panel renders inside the glass surface alongside the text area.
Callers can add a Divider between the text area and the panel if a visual
separator is desired. Not available on GlassTextField.search (that
constructor is single-line only; bottom is always null there).
0.12.3 #
🎨 Visual — Slider & Switch thumb refraction tuning #
GlassSliderthumb — increasedrefractiveIndex(1.15 → 1.3) andthickness(10 → 13) for a more pronounced glass lens feel. ReducedglassColoralpha (0.1 → 0.08),lightIntensity(2.0 → 1.8 premium),baseAlphaMultiplier(0.2 → 0.08 premium), andedgeAlphaMultiplier(0.4 → 0 premium) for a cleaner, more transparent thumb.GlassSwitchthumb — reducedrefractiveIndex(1.15 → 1.12) andglassColoralpha (0.1 → 0.08) for subtler refraction.- Material fade via
Opacitywidget — slider thumb now uses widget-levelOpacity(matchingGlassSwitchpattern) instead of color alpha for the press-down fade. Critical for Impeller: properly removes the child from the compositing tree so native refraction shows through.
⚡ Jelly physics — spring-based velocity #
GlassSliderjelly — replaced raw_velocitytracking with aSingleSpringControllerfeedingbuildJellyTransform. Produces smooth squash/stretch with natural deceleration and elastic bounce-back, matching the tab bar / bottom bar pill feel.maxDistortionraised (0.25 → 0.6),velocityScalelowered (30 → 2) to account for the spring's normalised 0→1 position range.
0.12.2 #
✨ New — GlassTextField enhancements #
Three community-requested features for GlassTextField (and GlassTextArea):
Explicit size properties #
height, minHeight, and maxHeight give direct control over the field's dimensions — no wrapping SizedBox needed:
// Fixed height — matches GlassSearchBar's 44pt:
GlassTextField(height: 44, placeholder: 'Search')
// Constrained range — grows with content:
GlassTextField(minHeight: 44, maxHeight: 200, maxLines: 10)
height is mutually exclusive with minHeight/maxHeight (assertion enforced).
onLineCountChanged callback #
Fires whenever the number of rendered lines changes (accounting for text wrapping, not \n characters). Also fires on initial build. Uses the TextField's own RenderBox height — no external TextPainter math, so it works correctly with text scaling and system accessibility settings.
GlassTextField(
maxLines: 6,
onLineCountChanged: (lines) {
setState(() => _borderRadius = lines > 1 ? 8.0 : 20.0);
},
)
iconAlignment parameter #
Controls where prefix/suffix icons sit when the field spans multiple lines:
// Pin send button to bottom — chat composer pattern:
GlassTextField(
maxLines: 6,
iconAlignment: CrossAxisAlignment.end,
suffixIcon: Icon(Icons.send),
)
Accepts CrossAxisAlignment.start (top), .center (default), or .end (bottom). No visible effect on single-line fields.
All three features are forwarded through GlassTextArea.
GlassTextField.search named constructor #
A new convenience constructor that pre-configures GlassTextField with compact search-bar defaults: height: 44, iconSpacing: 8, padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), borderRadius: 22, and textInputAction: TextInputAction.search. Eliminates the boilerplate previously required to match GlassSearchBar visuals:
GlassTextField.search(
placeholder: 'Search messages…',
prefixIcon: Icon(CupertinoIcons.search, size: 20),
useOwnLayer: true,
)
GlassSearchBar now uses this constructor internally, reducing duplicated decoration code.
🐛 Fix #
-
onLineCountChangednow fires on programmatic controller changes — Previously the callback only responded to physical keyboard input (TextField.onChanged). Settingcontroller.text = '...'or callingcontroller.clear()(e.g. in a chat "Send" handler) did not re-measure the line count. The widget now actively listens to theTextEditingControllerand re-measures on any text mutation. -
Suffix icon spacing now respects
iconSpacing— The gap before the suffix icon was hard-coded to 12px while the prefix icon correctly usedwidget.iconSpacing. Both sides now use the same parameter.
0.12.1 #
🐛 Fix — eliminate rectangular blur halo over PlatformViews (iOS Impeller) #
LightweightLiquidGlass and _FrostedFallback previously wrapped their glass surface in ClipPath(ShapeBorderClipper(...)). When the parent was an iOS PlatformView (e.g. mapbox_maps_flutter MapWidget, video_player on iOS), the descendant BackdropFilter's rectangular blur output leaked past the rounded clip — visible as a faint square halo around the rounded glass shape, most obvious when light backdrop content scrolled underneath.
Flutter framework PR #177551 (merged Dec 2025, shipped in 3.41.0-0.0.pre+) fixed this at the engine level by forwarding ClipRRect clip data to the iOS PlatformView mutator stack — but only ClipRRect, not ClipPath, even when the path inside is mathematically a rounded rect.
This release routes shapes that resolve to a RoundedRectangleBorder (i.e. LiquidRoundedSuperellipse, LiquidVerticalRoundedSuperellipse) through ClipRRect instead of ClipPath. The engine fix now triggers and the halo disappears for those shapes.
LiquidOval is intentionally NOT routed through ClipRRect — empirically the engine fix doesn't forward ClipRRect with circular(double.infinity) nor a LayoutBuilder-computed finite radius on the LiquidOval path. Callers that need a halo-free circular glass surface over a PlatformView should use LiquidRoundedSuperellipse(borderRadius: size / 2) instead, which renders identically on a square widget and triggers the engine fix.
Closes upstream Flutter #175048 and #115926 for liquid_glass_widgets users.
0.12.0 #
✨ New #
LiquidGlassWidgets.wrap() — theme parameter #
wrap() now accepts an optional GlassThemeData? theme parameter. When provided, it wraps the child in a GlassTheme — eliminating the need for a separate GlassTheme widget in your tree.
LiquidGlassSettings.standardOpacityMultiplier #
A new multiplier applied to the glass colour alpha when rendering in Standard mode. This allows tuning Standard 2D compositing opacity to achieve more parity with Premium 3D volumetric refraction without needing separate colour values for each mode.
LiquidGlassSettings(
glassColor: Colors.white.withValues(alpha: 0.3),
standardOpacityMultiplier: 0.4, // Standard renders at 0.3 × 0.4 = 0.12 alpha
)
Defaults to 1.0 (no change). Fully interpolated via LiquidGlassSettings.lerp() and wired through copyWith().
GlassPage — full-screen glass scaffold #
A new full-screen scaffold widget for glass-based layouts. Handles background imagery, status bar styling, and background sampling in a single widget.
enableBackgroundSampling defaults to true when a background widget is provided, and false otherwise — so the common case just works without extra configuration.
GlassPage(
background: Image.asset('assets/wallpaper.jpg'),
child: Scaffold(...),
)
Export hygiene #
glass_page.dartnow uses ashowclause: onlyGlassPageandGlassStatusBarStyleare exported (internal state classes are no longer public).liquid_glass_scope.dartnow uses ashowclause: onlyLiquidGlassScope,GlassBackgroundSource, andGlassRefractionSourceare exported.
🎨 Visual — Standard/Premium parity improvements #
Shader composite improvements (lightweight_glass.frag) #
The Standard-tier lightweight shader composite logic has been improved for closer visual parity with the Premium Impeller path. Shader rim constants are unchanged from 0.11.0 — AdaptiveGlass normalization now handles Premium → Standard scaling in Dart space instead:
- PATH A (background texture): now uses
applyGlassColorLW()— a luminosity-preserving glass tint that matches Premium's colour handling for both chromatic (mint, bronze) and achromatic (white, grey) glass colours. - PATH A ambient darkening:
ambientStrength × 0.25 + 0.08creates the glass shadow effect that visually separates glass from non-glass, matching what Premium achieves through blur compositing. - PATH A adaptive rim colour:
mix(bgRgb, white, 0.7)brightens the background at the edge, matching Premium'sgetHighlightColor. - PATH A edge-zone refraction: indicator-style background warping at rounded corners using
smoothstepedge zone with quadratic falloff — the same proven approach asinteractive_indicator.fragbut scaled for containers. Zero transcendentals (polynomialsmoothstep+ multiplies only). Flat interior pixels naturally produce zero offset. Currently active on surface widgets (GlassBottomBar,GlassTabBar,GlassSideBar,GlassToolbar) when abackgroundKeyis provided;GlassCardandGlassButtonuse PATH B and will benefit onceAdaptiveGlassgains scope-aware background key passthrough. - PATH A unified interactive glow:
uGlowIntensitypress-feedback now applies in PATH A, closing an architectural gap where switch/slider thumbs inside background-sampled containers had no glow. - Volumetric depth gradient: subtle top-to-bottom ambient shading (
+vertCoord × 0.04) in both PATH A and PATH B creates a natural 3D anchored depth feel, simulating light entering from above. Cost: one multiply + one add per fragment. - PATH B frost floor: 8% minimum material alpha ensures glass surfaces are always visible when
glassColor.a = 0(Premium default), preventing invisible glass in SrcOver compositing. - PATH B contrast-adaptive rim: shifts rim colour toward mid-grey on bright backgrounds so white-on-white borders remain distinguishable.
- Directional rim bonus: a small
0.15 × directionalInfluence × lightIntensityterm adds subtle lit-side variation on top of the constant rim base — matching how Premium's 3D bevel naturally brightens toward the light source.
Interactive widget transparency tuning #
GlassSwitchstandard thumb:baseAlphaMultiplier: 0.0,edgeAlphaMultiplier: 0.15— fully transparent body with subtle edge presence for a cleaner glass look.GlassSliderstandard thumb:baseAlphaMultiplier: 0.08,edgeAlphaMultiplier: 0.1— minimal body opacity with soft edge glow.
Elevated widget predictability #
Removed the arbitrary +0.2 alpha boost on elevated widgets inside AdaptiveGlass. Elevation is now expressed purely through the shader's densityFactor physics, making the opacity response predictable and proportional to user settings.
Interactive widget normalisation (GlassEffect) #
Standard-tier interactive indicators (slider thumbs, switch thumbs, segmented control pills) now apply the same normalisation as AdaptiveGlass — thickness × 0.4, lightIntensity × 0.6 — preventing the 2D shader from rendering these elements heavier than their Premium counterparts.
📦 Example app #
- Quality comparison demo background image bundled as a local asset (
example/assets/mountain_landscape.jpg) — eliminates network dependency and first-frame loading flash.
0.11.0 #
✨ New — Liquid Morph Engine (new architectural system) #
This release introduces the Liquid Morph Engine — a standalone, reusable physics and animation system for iOS 26-style liquid glass morphing. It lives in lib/engine/ and is fully decoupled from any specific widget.
GlassMenu is the first consumer of the engine. Future widgets (sheets, cards, buttons) will use the same engine to achieve consistent, physics-correct liquid glass transitions throughout the library.
Documentation:
docs/LIQUID_MORPH_ENGINE.md— full guide coveringGlassMorphController,LiquidMorphState,LiquidMorphPhysics, and how to integrate the engine into your own custom widgets.
Core engine types #
| Type | Role |
|---|---|
GlassMorphController |
Lifecycle owner — manages the spring, exposes open() / close() |
LiquidMorphState |
Immutable value object — one per frame, contains all render values |
MorphPhase |
Semantic lifecycle enum — tells you where in the animation you are |
MorphSpeed |
Enum — controls spring stiffness without exposing raw physics constants |
LiquidMorphPhysics |
Internal stateless math engine |
How it works #
Two conceptual "blobs" drive every morphing animation:
- Blob A (anchor) — the ghost trigger that shrinks away over the first 40 % of the animation, cleanly breaking the liquid bridge.
- Blob B (body) — travels from the trigger centre to the widget centre along a J-curve overshoot trajectory, expanding from trigger size to target size.
The SDF metaball shader creates the teardrop neck between the blobs automatically — there is no explicit neck geometry. LiquidMorphPhysics.compute() determines each blob's position, scale, and blend factor on every frame.
GlassMenu — first engine consumer #
- Teardrop open animation · The menu grows from the trigger point along the dual-curve SDF path, producing the iOS 26 "bubble emerging from button" effect.
- Rubber-band close physics · On dismiss the teardrop recoils with a critically-damped spring + overshoot tail, matching the tactile snap of native iOS context menus.
- Velocity-bump alignment · Spring initial velocity is seeded from touch velocity at release — fast flicks close snappily, slow releases settle deliberately.
- Handoff latching · Re-opening during a close animation inherits the in-flight velocity and reverses smoothly — no pop or cut.
- Blob scaling · Blob sizes scale relative to trigger size and computed menu height, so short and tall menus receive proportionally correct teardrop curvature.
See it live: The Apple Messages demo (
cd example && flutter run -t lib/apple_messages/apple_messages_demo.dart) showcases the morphing engine in a real-world context — tap the menu or Edit button at the top of the screen to trigger theGlassMenuwith full teardrop open/close physics.
Spring physics refinements #
- Critical damping (
ζ = 1.0) on all spring controllers prevents oscillation on rapid successive opens. interactionScale,stretch, andstretchResistanceintegrate into the morphing path via the same spring solver used byLiquidStretch.
🐛 Fixes #
-
GlassMenu— safe area / notch clipping on iOS and Android · Menu position and maximum height were computed fromMediaQuery.padding, which is consumed by ancestorSafeAreawidgets and reports0inside a fully-safe tree. Switched toView.of(context).padding(raw hardware insets) so the menu is always clamped correctly regardless ofSafeAreanesting depth. Fixes the menu appearing under the Dynamic Island on iPhone 14 Pro and similar devices. -
GlassMenu(scrollable) — scrolling now works on large menus · Menus with more items than fit on screen can now be scrolled reliably.
🗂 Example restructure — demos/ suite #
The example/ package has been reorganised for a cleaner public-facing demo experience:
-
New
example/lib/demos/folder containing seven self-contained, copy-pasteable demos:glass_menu_demo.dart— all 9GlassMenuAlignmentpositions, scrollable item listglass_tab_bar_scrollable_demo.dart— scrollableGlassTabBarwith dynamic tab addglass_modal_sheet_demo.dart— all sheet states (peek / half / full), Apple Maps peek styleglass_bottom_bar_demo.dart— magic-lens masking withGlassBottomBarbottom_bar_tab_width_demo.dart—tabWidthon both bar variants side-by-sidesearchable_bar_demo.dart—GlassSearchableBottomBaredge casesshape_debug_demo.dart—GlassButtonshape visualiser
-
Apple Messages demo (
example/lib/apple_messages/) — showcases the Liquid Morph Engine in a full real-world context; tap the menu or Edit button at the top to triggerGlassMenu. -
example/lib/modal_sheet_showcase/removed (file moved todemos/glass_modal_sheet_demo.dart). -
Experimental scratchpad scripts moved to git-ignored
example/lib/playground/.
0.10.10 #
Thanks to @g3mf0r for PR #55. 🙏
✨ New #
-
GlassMenu—menuAlignmentenum · A newGlassMenuAlignmentenum (10 values:none,topLeft,topCenter,topRight,centerLeft,center,centerRight,bottomLeft,bottomCenter,bottomRight) lets you pin the menu to a specific edge or corner of its trigger instead of relying solely on auto-detection. The enum is now part of the public API surface exported fromglass_menu.dart. -
GlassMenu—autoAdjustToScreenwithmenuPadding· WhenautoAdjustToScreen: true, the newmenuPadding: EdgeInsets?parameter applies additional inset constraints so the menu body never clips against device edges. -
GlassMenu—itemBorderRadius· Controls the corner radius of individual menu item cells, independent of the outermenuBorderRadius.
🐛 Fixes #
-
GlassTabBar— multi-tab drag jump · Dragging the indicator across multiple tab widths in a single gesture now snaps to the correct distant tab. The previous implementation only incremented/decremented by ±1 regardless of drag distance, causing the indicator to teleport unexpectedly when the finger crossed more than one tab boundary. -
GlassTabBar— glass refraction during indicator drag · Refraction and shadow effects are correctly suppressed during the drag animation and restored on settlement, eliminating a visual glitch where the glass distortion would persist after releasing the indicator.
🧪 Tests #
- Added 4 new
GlassMenutests coveringGlassMenuAlignmentenum values,menuAlignmentparameter,autoAdjustToScreen+menuPadding, anditemBorderRadius. - Added 2 new
GlassTabBartests covering multi-tab drag jump (left and right) to prevent regression of the PR #55 fix.
0.10.9 #
Thanks to @g3mf0r for PR #54. 🙏
✨ New #
GlassTabBar(scrollable) — jelly physics on indicator drag · The scrollable indicator pill now feeds real-time drag velocity into the liquid glass shader, producing the same organic stretch-and-settle effect that fixed-mode tabs already had.
🐛 Fixes #
-
LiquidGlassWidgets.wrap—adaptiveQuality: truewithoutadaptiveConfigpermanently locks tostandard· The default fallback config was created withinitialQuality: GlassQuality.standard, which the adapter treats as a skip-Phase-2 signal — immediately jumping to Phase 3 atstandardwithout ever running the warmup benchmark. On capable devices (including the iPhone simulator on Apple Silicon) this prevented the adapter from ever discovering that the device can sustainpremium. Fixed by removing the erroneousinitialQualityfrom the fallback; Phase 2 now always runs when no explicit quality is provided. -
GlassTabBar(scrollable) — indicator overflows bar on low tab counts · The right drag boundary was computed asviewMaxinstead ofviewMax - indicatorWidth, allowing the pill to slide outside the bar when there were only 2–3 wide tabs. Corrected toviewMax - targetWidth. -
GlassTabBar(fixed) — tiny accidental drags switch tabs · Tab switching on drag-end now requires either a displacement greater than 20 % of the tab width or a flick velocity above 400 px/s, preventing unintended switches from small incidental movements. -
GlassTabBar(scrollable) — flick gesture ignored in scrollable mode · A horizontal flick with sufficient velocity now overrides the nearest-tab distance calculation and advances the indicator in the flick direction, matching the fixed-mode behaviour.
0.10.8 #
Thanks to @g3mf0r for PR #52. 🙏
🐛 Fixes #
-
GlassTabBar— indicator drag drift on desktop/web · The indicator position was accumulated viadelta.dxadditions each frame, causing the pill to visually lag behind the pointer on desktop platforms where pointer events arrive at a higher frequency than the frame budget. Fixed by computing position from the absolute global pointer coordinate on every update event, eliminating accumulated drift. -
GlassTabBar(scrollable) — tab labels hidden behind indicator pill · TheSingleChildScrollView(tab labels) and the background pill were inserted in the wrong stack order — labels were painted first, then the pill on top, obscuring them. Fixed by inserting the pill before the labels so labels always paint above the pill (correct z-order). -
GlassTabBar(scrollable) — indicator fly-off past bar edges · The indicator pill had no boundary clamping in scrollable mode, allowing it to animate outside the visible bar area. Drag offset is now clamped to[scrollOffset - 35 %, scrollOffset + screen + 35 %].
✨ New #
-
DividerSettings— new optionaldividerSettingsparameter onGlassTabBar. Renders animated vertical dividers between tabs with configurablethickness,indent,endIndent, customdecoration, animationduration/curve, and anisHideAutomaticallyflag that fades out dividers adjacent to the active tab. Includes acopyWithhelper for convenient inline customisation. -
Grab-to-drag in scrollable mode — the indicator pill in scrollable mode can now be directly grabbed and dragged to a new tab. Uses a
GestureArenaTeam(HorizontalDragGestureRecognizeras captain +TapGestureRecognizer) to correctly win the arena against theSingleChildScrollViewwhen the initial touch is within the active indicator's bounds. The scroll view retains natural scrolling behaviour when touching outside the pill. -
indicatorShadow— new optionalindicatorShadow: List<BoxShadow>?parameter onGlassTabBar. Applies a drop shadow to the resting (solid-colour) indicator pill, improving contrast in light-mode themes where the pill and track share similar colours. The shadow is automatically suppressed during the liquid glass drag animation so it does not interact with the backdrop blur, and restored when the pill returns to its idle state.
0.10.7 #
Thanks to @yukinoaruu for PR #51. 🙏
🐛 Fixes #
GlassMenu— trigger button dead zone after closing · After closing the menu, the trigger button would ignore taps for the duration of the closing spring animation (~95% of travel), forcing the user to wait several seconds before being able to reopen it. Fixed by separating the visual-hide threshold (0.05) from the input-block threshold (0.80) into two independent booleans:isButtonVisibleandisMenuBlocking. The button now becomes tappable again as soon as the animation drops below 80%, and the morphing glass overlay wraps inIgnorePointer(ignoring: value < 0.8)to prevent the contracting container from consuming the tap instead.
0.10.6 #
🐛 Fixes #
GlassBottomBar—extraButtoncauses bar to float in the middle of the screen · Wrapped the innerRowin aSizedBox(height: barHeight)so theScaffold.bottomNavigationBarslot always receives an explicit tight height constraint. Without this, theExpandedchild introduced byextraButtonpropagated an unbounded height throughLiquidGlassLayer, causing Flutter to render the bar centred on screen instead of pinned to the bottom edge.
0.10.5 #
✨ New #
SearchableBottomBarController— addedopenSearch(),closeSearch(), andisSearchOpengetter for programmatic search control. Previously the only way to open search was by drivingisSearchActivefrom parent state.GlassTabBar— addedmaskingQualityparameter (MaskingQuality.high/MaskingQuality.off), matching the existingGlassBottomBarAPI. Set tooffto disable the 8 px jelly-bloom expansion on lower-end devices.GlassSlider— addedinteractionBehavior,glowColor, andglowRadiusfor consistent drag-glow customisation across all interactive widgets.GlassSegmentedControl— sameinteractionBehavior,glowColor,glowRadiusparams added for API parity withGlassSliderandGlassTextField.LiquidGlassWidgets.respectsAccessibility— deprecated alias added pointing torespectSystemAccessibility. Will be removed in v1.0.
🐛 Fixes #
GlassTextField— fixed a use-after-dispose crash whenfocusNodecyclednull → external → null. The widget now tracks ownership with an explicit_ownsNodeflag and correctly creates a fresh internal node on each transition.GlassTabBar— resolved scrollable-mode visual glitches. The indicator now stays perfectly glued to the active tab during scrolling without drifting, uses native "snappy" spring physics for consistent feel, and implements a three-layer rendering architecture so the solid indicator pill cleanly clips at the rounded viewport corners while the 8px jelly bloom expands freely over the tab bar boundaries.
0.10.4 #
A huge, heartfelt thank-you to @yukinoaruu for PR #49. 🙏
We made a mess of the original 0.10.3 merge of his work — introducing regressions that broke the very things he had so carefully built. He came straight back, fixed every issue, and did it with incredible patience and generosity. This release is entirely his. If you are enjoying GlassMenu, it is because of him.
🐛 Fixes (regressions from 0.10.3 merge) #
- GlassMenu — full-list rebuilds on every pointer event · Restored the
_cachedWrappedItemsmechanism that was accidentally dropped. The previous merge caused the entire wrapped-item list to be recreated on each pointer move, resetting pressed/hover states mid-gesture and tanking performance on menus with many items. - GlassMenu — selection and hover state precision · Migrated to a
ValueNotifiersystem (_hoveredIndexNotifier,_isDraggingNotifier). Individual menu items now rebuild in isolation instead of triggering a fullsetStateon the entire menu tree, keeping animations at a steady 60 fps. - GlassMenu — ghosting on selection pill · Fixed a double-background artifact where the selected
GlassMenuItempainted its own hover fill on top of the sliding pill, producing a faint ghost ring. Selected items now transition toColors.transparentinstantly. - GlassMenu — disabled items could be tapped · Tapping a disabled item no longer calls
onTapor closes the menu. The pill highlight correctly skips disabled items during pointer tracking. - GlassMenu — double
onTapfiring · Removed a redundantonTapcallback in the internal wrapped-item builder that was causing every selection to fire twice. - GlassMenu —
RangeErrorwhen item list shrinks while open ·didUpdateWidgetnow clears_hoveredIndexwhenitems.lengthdecreases, preventing an out-of-bounds crash when the pill tried to measure a deleted item. - GlassMenuItem — disabled opacity · Disabled items now render at
Opacity(0.4)to match the design spec and test expectations. - GlassMenuLabel — hybrid
title/childAPI ·GlassMenuLabelnow accepts either atitleString (rendered as stylised uppercase) or an arbitrarychildWidget, enabling diverse non-interactive content beyond simple section headers. - GlassMenuLabel —
heightdefault · Defaultheightset to30.0so the selection pill cannot drift when items with non-standard font sizes are mixed in. - GlassMenu —
glowIntensityparameter · AddedglowIntensityand wired it through toGlassContainer, completing the full interaction-glow parameter surface. - GlassMenu —
glowOnTapOnlydefault corrected · Default changed totrueto prevent a permanently stuck glow artefact during scroll and drag gestures. - GlassMenu — stretch parameter rename · Renamed
allowPositiveXStretch/allowNegativeXStretch/allowPositiveYStretch/allowNegativeYStretchtoallowPositiveX/allowNegativeX/allowPositiveY/allowNegativeYto align with theLiquidStretchAPI surface. - GlassMenu — compositing architecture · Removed redundant
RepaintBoundarynodes that were leaving descendant glass layers DETACHED from the compositor scene, and movedGlassGlowinsideGlassContainer's clip subtree to prevent glow bleed onto the background.
⚠️ Breaking — GlassMenu stretch parameter renames #
The four optional stretch-axis override parameters introduced in 0.10.3 have been renamed:
| 0.10.3 name | 0.10.4 name |
|---|---|
allowPositiveXStretch |
allowPositiveX |
allowNegativeXStretch |
allowNegativeX |
allowPositiveYStretch |
allowPositiveY |
allowNegativeYStretch |
allowNegativeY |
All four remain optional with null defaults (auto-inferred from menu position). Only code explicitly passing the old names needs updating.