adaptive_liquid_glass

iOS 26 Liquid Glass for Flutter, measured against SwiftUI, with Material 3 counterparts on Android, behind one API. App code never branches on platform.

  • iOS 26+: SwiftUI's own Liquid Glass, pixel-identical to a SwiftUI app.
  • Older iOS: a fragment-shader glass tuned to SwiftUI's.
  • Android: a Material 3 surface (colours from your Theme, ink ripple for interactive glass). No glass code runs.
  • RTL-safe: directional insets throughout; the light angle is not mirrored, matching iOS.

Adaptive Liquid Glass demo on iOS 26

Screenshots

The screenshot gallery shows the example app on iOS 26.4 in light and dark modes. Plain backgrounds make the controls easier to inspect; photo backgrounds show the glass effect over imagery.

controls alert toolbar sheet pickers fields lists chips progress gauges badges empty

Fidelity

The shipped glass constants are certified against Apple's SwiftUI .glassEffect on the reference simulator (iPhone 17 Pro, iOS 26.4):

path result
Shader (iOS < 26, or mode: shader) every scene at SSIM ≥ 0.95 (minimum 0.9525); 59/75 pass the strict bars (SSIM ≥ 0.97 and ΔE ≤ 2.0); median SSIM 0.983, median ΔE 1.19
Native (iOS 26+, the default) 75/75 at parity with SwiftUI (minimum SSIM 0.9996, maximum ΔE 0.05)

Over 75 scenes (regular, clear and tinted glass; capsules, circles and rounded rectangles; photo, text and gradient backgrounds; light and dark; merged groups): 59/75 scenes pass the official bars (SSIM ≥ 0.97, CIEDE2000 ΔE ≤ 2.0), median SSIM 0.9830 / median ΔE 1.19, min SSIM 0.9525, 0 scenes below SSIM 0.95 on the 75 in-set scenes (baseline build/fidelity/g8-3). Model↔Flutter parity is 75/75. The full per-scene record, diagnosis and known residuals: docs/superpowers/notes/fidelity-status.md.

Press and morph motion uses SwiftUI's measured springs (press-in, release and .bouncy morph) and a size-dependent press growth. To re-run, follow tool/fidelity/README.md: capture both renderers with tool/fidelity/capture.sh build/fidelity/<run> (setup and options documented there), then score with tool/fidelity/.venv/bin/python tool/fidelity/compare.py build/fidelity/<run>.

Quick start

Install

dependencies:
  adaptive_liquid_glass: ^0.1.3
import 'package:adaptive_liquid_glass/adaptive_liquid_glass.dart';

No setup is needed: no theme, no initialisation call, no platform checks.

Setup (optional)

Without it, the shaders load on first use: glass draws as a plain blur for a frame or two before switching over in place, and the scroll edge and tab lens use their fallbacks until ready. AdaptiveLiquidGlass.initialize() loads them all before the first frame; it is a no-op on Android's Material path and safe to call twice.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await AdaptiveLiquidGlass.initialize();
  runApp(const MyApp());
}

One line

const Text('Hello').glassEffect(padding: const EdgeInsets.all(12));

// A glass button, also one line:
const Icon(Icons.add).glassEffect(onPressed: add);

glassEffect mirrors SwiftUI's .glassEffect(_:in:) and returns a LiquidGlass widget. Text and icons on glass get a readable colour by default, like SwiftUI's vibrant labels; colours you set yourself still win.

The full widget takes the same options and a few more:

LiquidGlass(
  glass: Glass.regular.tint(Colors.blue),   // default: Glass.regular
  shape: const GlassShape.rect(20),         // default: capsule
  padding: const EdgeInsetsDirectional.all(16),
  onPressed: () {},                         // makes it a button
  child: const Text('Continue'),
)

Glass has named presets when you don't want to compose one:

Preset Means
Glass.regular / Glass.frosted standard frosted glass (the default)
Glass.clear / Glass.crystal highly transparent, for photos and video
Glass.smoke regular glass darkened with a smoke tint
Glass.accent regular glass tinted iOS 26 system blue
Glass.tinted(color) shorthand for Glass.regular.tint(color)

Every preset still chains: Glass.smoke.interactive().

With onPressed the glass is a button: it gets button semantics, takes focus, answers Enter and Space, and its glass turns interactive (the press stretch and glow on iOS, the ripple on Android). Pass Glass.regular.interactive(false) to keep the tap without the press visuals.

A group of buttons

Glass next to glass belongs in one GlassGroup, SwiftUI's GlassEffectContainer. Members are drawn in one pass, sample the same backdrop, and can blend and morph into each other.

GlassGroup(
  spacing: 16,
  child: Row(
    mainAxisSize: MainAxisSize.min,
    spacing: 12,
    children: [
      LiquidGlass(
        shape: const GlassShape.circle(),
        padding: const EdgeInsets.all(14),
        onPressed: () {},
        child: const Icon(Icons.edit),
      ),
      LiquidGlass(
        shape: const GlassShape.circle(),
        padding: const EdgeInsets.all(14),
        onPressed: () {},
        child: const Icon(Icons.share),
      ),
    ],
  ),
)

spacing is how close two shapes must be to flow together. The default, 0, merges only shapes that touch, so a row of buttons stays a row of separate buttons. Set it to at least the gap between them (here 12) for SwiftUI's liquid blending.

Theming and native mode

A LiquidGlassTheme is optional; without one the defaults apply.

LiquidGlassTheme(
  data: const LiquidGlassThemeData(
    defaultGlass: Glass.clear,  // glass for widgets that do not pick one
  ),
  child: MaterialApp(home: const HomePage()),
)

Glass and the text on it follow one brightness — the theme's (a MaterialApp with no dark theme gets light glass on a dark system; set a dark theme and the glass goes dark with it).

On iOS 26 and later, glass is SwiftUI's own Liquid Glass: each GlassGroup hosts one GlassEffectContainer with a .glassEffect view per member in a platform view behind its content, so it matches a SwiftUI app pixel for pixel (including dark mode, tint, merging and Reduce Transparency, which SwiftUI handles itself). There is no flag: the iOS version is read at startup, so the first frame already uses it. Older iOS gets the shader. To force a path for one subtree, pass mode: to GlassGroup or LiquidGlass (GlassRenderMode.auto, shader, native or material); set defaultMode: GlassRenderMode.shader in the theme to opt out of native glass app-wide.

Android

On Android, and any platform other than iOS, auto renders Material 3:

Glass Material 3
Glass.regular surfaceContainerHigh, elevation 1
Glass.clear surfaceContainerLow at 85 % opacity
.tint(color) primaryContainer of ColorScheme.fromSeed(color)
.interactive() / onPressed InkWell ripple
GlassGroup layout only (no merging)

Text and icons on the surface use its matching "on" colour (onSurface, or the tint's onPrimaryContainer).

Which widget?

Every component is iOS 26's own on iOS and gets a Material 3 counterpart on Android. All are in the example app's Gallery (example/lib/gallery.dart, flutter run -t lib/gallery.dart).

Widget / function What it is Material 3 on Android
LiquidGlass, GlassEffect.glassEffect Glass behind any child; SwiftUI's .glassEffect(_:in:) Surface (table above)
Glass The material: regular, clear, .tint(), .interactive() Variant mapping (table above)
GlassShape The outline: capsule, circle, rect(r), concentric() Same shape as a border
GlassGroup Neighbouring glass drawn in one pass; blends and morphs Layout only (no merging)
LiquidGlassTheme, LiquidGlassThemeData Optional app-wide defaults (defaultGlass, defaultMode, lightAngle) —
GlassRenderMode Force auto, shader, native or material for a subtree —
AdaptiveLiquidGlass.initialize Optional: preload the shaders in main() no-op
GlassBackdropSource, GlassForeground Adaptive label colour over busy content —
GlassSystemColors iOS 26's red, blue, orange and green —
GlassButton (+ .icon), GlassButtonRole, GlassButtonStyle, GlassButtonShape, GlassControlSize, GlassControlSizeScope SwiftUI's glass and prominent buttons, roles, sizes, shapes, loading FilledButton / IconButton / TextButton
GlassBackButton Circular glass chevron that pops the route BackButton
GlassNavigationBar, SliverGlassNavigationBar, GlassScrollEdgeStyle Inline and large-title nav bars, no bar background; scrollEdgeStyle blurs content scrolled under AppBar / SliverAppBar.large
GlassScaffold, GlassBottomAccessory An iOS 26 screen in one widget; the Music-style mini-player slot Same layout, Material bars
GlassToolbar, GlassToolbarSpacer The floating bottom toolbar, merged capsules Material buttons
GlassTabBar, GlassTabBarItem iOS 26's floating tab bar, draggable lens, onSearch tab NavigationBar
GlassToggle iOS 26's switch, glass lens thumb Switch
GlassSlider Slider with a glass lens thumb, optional divisions Slider
GlassSegmentedControl, GlassSegment Segmented control, sliding lens SegmentedButton
GlassStepper Minus/plus capsule stepper pair of icon buttons
GlassPicker, GlassPickerItem Menu-style picker, current choice checked DropdownButton
GlassWheelPicker .pickerStyle(.wheel): scrolling rows under a clear-glass selection band, haptics ListWheelScrollView on a Material surface
GlassDatePicker Compact date picker opening a glass calendar text button + showDatePicker
GlassMenuButton, GlassMenuItem Pull-down glass menu from an icon button MenuAnchor
GlassMenuController Drives a menu from an outer gesture: slide-to-select (glideTo) open/close only
GlassContextMenu Long-press lifts the child and opens the glass menu popup menu
GlassPopoverAnchor, showGlassPopover SwiftUI's iPhone popover: a glass bubble over its anchor Material surface bubble
showGlassAlert, GlassDialogAction iOS 26 alert: centred glass card, buttons side by side AlertDialog
showGlassConfirmationDialog Small glass card, actions stacked; cancel taken by a tap outside AlertDialog
showGlassActionSheet iOS 26's confirmation dialog on iPhone: stacked capsule buttons modal bottom sheet
showGlassSheet, GlassSheet, GlassSheetDetent Modal sheet with detents (medium, large, fraction, height) modal bottom sheet
showGlassFullScreenCover, GlassFullScreenCoverHandle .fullScreenCover: opaque edge-to-edge modal sliding up full-screen Dialog
showGlassToast, GlassToastAction, GlassToastHandle Glass capsule toast from the top, queued, swipe to dismiss SnackBar
GlassTextField (+ .password) Text field in a glass capsule; password with an eye button TextField
GlassSearchField Search capsule with magnifier and clear button SearchBar
GlassSearchable, GlassSearchController, GlassSearchSuggestion .searchable: field with a springing Cancel, suggestions on a glass platter, scopes; bottom or navigation-bar placement SearchAnchor
GlassListSection, GlassListTile Inset-grouped list, like Settings; rows with leading/title/value/trailing Card of ListTiles
GlassDisclosureGroup SwiftUI's DisclosureGroup: a list row with a rotating chevron that expands its rows ExpansionTile
GlassEmptyState (+ .search) ContentUnavailableView: icon, title, description, actions plain centred column
GlassChip Capsule chip with selected state and delete button FilterChip / InputChip
GlassBadge Red count capsule (dot when empty) on a child's top trailing corner Badge
GlassActivityIndicator iOS's spinner on its own (ProgressView() with no value) CircularProgressIndicator
GlassPageControl Page dots on a glass capsule; tap and scrub row of Material dots
GlassProgressIndicator, GlassProgressStyle Linear bar on a glass track, or ring/spinner LinearProgressIndicator / CircularProgressIndicator
GlassGauge, GlassGaugeStyle SwiftUI's Gauge: linear capacity, accessoryCircular, accessoryCircularCapacity LinearProgressIndicator / CircularProgressIndicator
GlassSwipeActions, GlassSwipeAction List swipe actions: tinted capsules, full swipe, haptic iOS layout, Material colour

Cookbook

One minimal example per component. All are in the example app's Gallery (example/lib/gallery.dart) and example/lib/components_demo.dart (flutter run -t lib/components_demo.dart).

Glass button

const Text('Save').glassEffect(
  onPressed: save,
  padding: const EdgeInsetsDirectional.symmetric(horizontal: 24, vertical: 14),
)

GlassButton is SwiftUI's .buttonStyle(.glass) and .glassProminent, with roles, control sizes, border shapes, and disabled and loading states:

GlassButton(onPressed: save, child: const Text('Save'))
GlassButton(
  onPressed: delete,
  role: GlassButtonRole.destructive,
  style: GlassButtonStyle.glassProminent,
  child: const Text('Delete'),
)
GlassButton.icon(onPressed: share, icon: CupertinoIcons.share, semanticLabel: 'Share')
GlassButton(onPressed: upload, loading: uploading, child: const Text('Upload'))

Sizes are measured from SwiftUI on iOS 26.4 (iOS 26 draws mini as small and extraLarge as large; icon-only buttons are capsules 12 pt wider than tall). Set a default with GlassControlSizeScope(size: GlassControlSize.small, ...).

Card with padding

const LiquidGlass(
  shape: GlassShape.rect(24),
  padding: EdgeInsetsDirectional.all(20),
  child: Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('Liquid Glass', style: TextStyle(fontSize: 22)),
      Text('Text on glass picks a readable colour.'),
    ],
  ),
)

Nested glass with GlassShape.concentric() follows the card's corners, inset by the distance between them.

Morphing pair with glassId

Give glass an identity and it morphs in and out of its neighbours as it appears and disappears, like glassEffectID:

GlassGroup(
  spacing: 20,
  child: Row(
    mainAxisSize: MainAxisSize.min,
    spacing: 12,
    children: [
      LiquidGlass(
        glassId: 'toggle',
        shape: const GlassShape.circle(),
        padding: const EdgeInsets.all(14),
        onPressed: () => setState(() => expanded = !expanded),
        child: Icon(expanded ? Icons.close : Icons.add),
      ),
      if (expanded)
        const Icon(Icons.favorite).glassEffect(
          glassId: 'extra',
          shape: const GlassShape.circle(),
          padding: const EdgeInsets.all(14),
        ),
    ],
  ),
)

Merged union

Members with the same unionId are drawn as one shape however far apart they are, like glassEffectUnion:

GlassGroup(
  child: Row(
    mainAxisSize: MainAxisSize.min,
    spacing: 24,
    children: [
      for (final icon in [Icons.edit, Icons.share, Icons.delete])
        Icon(icon).glassEffect(
          shape: const GlassShape.circle(),
          unionId: 'tools',
          padding: const EdgeInsets.all(12),
        ),
    ],
  ),
)

Clear glass over media

Glass.clear is the highly transparent variant for photos and video:

Stack(
  alignment: Alignment.center,
  children: [
    Image.asset('cover.jpg'),
    const Icon(Icons.play_arrow, size: 36).glassEffect(
      glass: Glass.clear.interactive(),
      shape: const GlassShape.circle(),
      padding: const EdgeInsets.all(16),
    ),
  ],
)

Tinted glass

LiquidGlass(
  glass: Glass.regular.tint(Colors.blue),
  onPressed: () {},
  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
  child: const Text('Tinted'),
)

Tab bar

iOS 26's floating tab bar. Pressing turns the selected pill into a clear glass lens that magnifies the tabs under it; dragging slides it between tabs; letting go springs it to the nearest tab and calls onSelected.

Stack(
  children: [
    pages[tab],
    Positioned(
      left: 21,
      right: 21,
      bottom: 21,
      child: Center(
        child: GlassTabBar(
          items: const [
            GlassTabBarItem(icon: CupertinoIcons.clock_fill, label: 'History'),
            GlassTabBarItem(
              icon: CupertinoIcons.text_quote,
              label: 'Snippets',
              badge: '3', // '' shows a dot
            ),
            GlassTabBarItem(icon: CupertinoIcons.gear_solid, label: 'Settings'),
          ],
          selectedIndex: tab,
          onSelected: (i) => setState(() => tab = i),
        ),
      ),
    ),
  ],
)

Tabs sit itemWidth (86.0, as on iOS) apart and move closer evenly when the bar would not fit, so five tabs fit a phone. Press, drag and release follow iOS 26's measured springs: the lens trails the finger and wobbles with its acceleration. The lens is drawn with the package's shader, fitted to iOS 26.4's tab lens: unfrosted, it refracts what lies just outside its edge into a band and splits colour there. So that the lens can refract the bar, the bar is shader glass too whenever the shader is available (pass mode: to force another path). Give a tab an activeIcon for its selected state. The selected tab takes selectedColor (default: iOS 26's tab bar blue, measured from SwiftUI's TabView); the others take glass's readable colour. onSearch adds iOS 26's search tab, a glass circle beside the bar. On Android it is a Material 3 NavigationBar in the same floating capsule. Each tab is a selectable button for VoiceOver and TalkBack, the order follows the reading direction, and with Reduce Motion the pill moves without animating.

// Inline title
Scaffold(
  extendBodyBehindAppBar: true,
  appBar: GlassNavigationBar(
    title: const Text('Inbox'),
    scrollEdgeStyle: GlassScrollEdgeStyle.progressive, // optional
    actions: [GlassButton.icon(
      onPressed: share, icon: CupertinoIcons.share, semanticLabel: 'Share')],
  ),
  body: ...,
)

// Large title
CustomScrollView(slivers: [
  SliverGlassNavigationBar(largeTitle: const Text('Inbox'), actions: [...]),
  SliverList(...),
])

No bar background, as on iOS 26: a GlassBackButton when the route can pop, the actions merged into one glass capsule, and a fade at the top once content is under the bar. GlassScrollEdgeStyle.progressive is an opt-in graduated blur under the bar (strongest at the top, easing to sharp further down); the default uniform is one even blur. The large title scrolls away with the content, the inline title fades in once it has gone, and pulling down stretches the large title. Metrics are measured from SwiftUI's NavigationStack (tool/reference/). On Android these are AppBar and SliverAppBar.large.

Screen layout

GlassScaffold(
  navigationBar: const GlassNavigationBar(title: Text('Inbox')),
  tabBar: GlassTabBar(items: tabs, selectedIndex: tab, onSelected: pick),
  bottomAccessory: GlassBottomAccessory(child: nowPlaying), // optional
  body: ListView(children: rows),
)

One widget lays out an iOS 26 screen. The body runs under the bars. The tab bar floats 21 pt above the home indicator, and an optional accessory floats above it, like Music's mini-player. The body's MediaQuery padding grows by the bars, so scroll views keep their content clear of them. Read that padding inside the body, not with a context from above the scaffold. The body is a GlassBackdropSource unless sampleBackdrop: false.

Toolbar

GlassScaffold(
  toolbar: GlassToolbar(children: [
    GlassButton.icon(onPressed: reply, icon: CupertinoIcons.reply,
        semanticLabel: 'Reply'),
    const GlassToolbarSpacer(),
    GlassButton.icon(onPressed: compose, icon: CupertinoIcons.pencil,
        semanticLabel: 'Compose'),
  ]),
  body: ...,
)

Neighbouring items merge into one glass capsule. A GlassToolbarSpacer starts a new capsule and pushes the groups apart, like SwiftUI's ToolbarSpacer.

Toggle, slider, segmented control, stepper

GlassToggle(value: wifi, onChanged: (v) => setState(() => wifi = v))
GlassSlider(value: volume, onChanged: (v) => setState(() => volume = v))
GlassSegmentedControl<Period>(
  segments: const [
    GlassSegment(value: Period.day, label: Text('Day')),
    GlassSegment(value: Period.week, label: Text('Week')),
  ],
  selected: period,
  onChanged: (p) => setState(() => period = p),
)
GlassStepper(value: count, min: 0, max: 10, onChanged: (v) => setState(() => count = v))

As in iOS 26, each thumb turns into a clear glass lens while it is pressed or dragged. The segmented control's lens slides between segments with a haptic on each one. All four follow the reading direction and Reduce Motion. On Android they are Switch, Slider, SegmentedButton and a pair of icon buttons.

Text field and search field

GlassTextField(
  placeholder: 'Email',
  clearButton: true,
  onChanged: (text) => setState(() => email = text),
)

const GlassTextField.password(
  placeholder: 'Password',
  onSubmitted: logIn,
)

GlassSearchField(onChanged: (q) => setState(() => query = q))

The text field is a glass capsule (a rounded rect when multi-line) with an optional prefix and suffix, a clear button, an errorText under the field, and a semanticLabel for screen readers. .password obscures the text and adds an eye button that shows and hides it. Put the search field in a GlassBottomAccessory slot or a toolbar for iOS 26's bottom search. On Android they are Material 3's TextField and SearchBar.

For SwiftUI's .searchable presentation wrap the screen's content (a GlassScaffold body) in GlassSearchable: tapping the field springs a Cancel button out of its end, suggestionsBuilder rows show on a glass platter and scopes become a GlassSegmentedControl while search is active. placement picks iPhone's floating bottom field or the navigation-bar one; a GlassSearchController reads or drives the text, activation and scope. On Android it is Material 3's SearchAnchor.

GlassSearchable(
  controller: search,
  scopes: const ['All', 'Recent'],
  suggestionsBuilder: (context, search) => [
    for (final hit in hits(search.text))
      GlassSearchSuggestion(title: Text(hit), onSelected: () => open(hit)),
  ],
  child: const MailList(),
)

List section

GlassListSection(
  header: const Text('General'),
  children: [
    GlassListTile(
      leading: const Icon(CupertinoIcons.wifi),
      title: const Text('Wi-Fi'),
      trailing: GlassToggle(value: wifi, onChanged: setWifi),
    ),
    GlassListTile(
      title: const Text('About'),
      value: 'iOS 26.4',
      chevron: true,
      onTap: openAbout,
    ),
  ],
)

iOS 26's inset-grouped list, measured against SwiftUI on iOS 26.4: rows on a rounded platter with hairline separators and optional header and footer. The platter is opaque by default, as iOS 26 Settings; pass glass: Glass.regular for a Liquid Glass platter over imagery. A tile's row highlights while pressed. On Android it is a Material 3 filled Card of ListTiles.

Disclosure group

GlassListSection(
  header: const Text('Settings'),
  children: [
    GlassDisclosureGroup(
      leading: const Icon(CupertinoIcons.gear),
      label: const Text('Advanced'),
      initiallyExpanded: true,
      children: const [
        GlassListTile(title: Text('Proxy'), value: 'Off'),
        GlassListTile(title: Text('DNS'), value: 'Automatic'),
      ],
    ),
  ],
)

A list row that expands to reveal more rows beneath it, like SwiftUI's DisclosureGroup: the label row lays out as a list row whose chevron rotates to point down while the children, indented, spring open, and it keeps the list's press highlight. Leave the state internal with initiallyExpanded, or drive it with isExpanded and onExpansionChanged. Geometry measured against SwiftUI on iOS 26.4. On Android it is a Material 3 ExpansionTile.

Chip and badge

GlassChip(
  label: 'Whale watching',
  icon: CupertinoIcons.tag,
  selected: picked,
  onSelected: (v) => setState(() => picked = v),
  onDeleted: () => remove('Whale watching'),
)

GlassBadge.count(count: unread, child: const Icon(CupertinoIcons.mail))

The chip is a glass capsule with a selected state (tinted glass) and an optional delete button; without onSelected it is a display-only tag. The badge is iOS's small solid red capsule with a count (a dot when empty, hidden at 0) on a child's top trailing corner — iOS draws badges solid, not glass. Geometry estimated, not yet measured. On Android: FilterChip / InputChip and Badge.

Page control and progress

GlassPageControl(
  count: 3,
  controller: pageController,
  onPageChanged: (page) => debugPrint('page $page'),
)

GlassProgressIndicator(value: downloaded)   // 0..1, null indeterminate
const GlassProgressIndicator.circular()     // a spinner

Tapping a half of the page control steps a page and dragging scrubs the dots, like UIKit's interactive page control. The progress indicator is a linear bar on a glass track, or a circular ring (iOS's activity spinner while indeterminate); the fill defaults to system blue. Geometry estimated, not yet measured. On Android: a row of Material dots, and Material 3 progress indicators.

Empty state

GlassEmptyState(
  icon: const Icon(CupertinoIcons.tray),
  title: const Text('No Mail'),
  description: const Text('New messages you receive will appear here.'),
  actions: [GlassButton(onPressed: refresh, child: const Text('Refresh'))],
)

GlassEmptyState.search(query: 'kiwi')

SwiftUI's ContentUnavailableView: a centred icon, title, description and stacked actions where a list or a search has nothing to show. .search draws the magnifying glass, "No Results for “kiwi”" and a hint, so an empty search is one line (searchTitle and searchDescription are exposed to localise them). Actions are small buttons unless a GlassControlSizeScope above says otherwise. It draws no platter of its own and nothing animates. Geometry measured against SwiftUI on iOS 26.4. On Android the same column takes Material 3 typography.

Gauge

GlassGauge(
  value: 0.62,
  label: const Text('Battery'),
  currentValueLabel: const Text('62%'),
  minimumValueLabel: const Text('0'),
  maximumValueLabel: const Text('100'),
)
GlassGauge(
  value: 21, min: 0, max: 40,
  style: GlassGaugeStyle.accessoryCircular,
  label: const Text('Temp'),
  currentValueLabel: const Text('21'),
  tint: GlassSystemColors.orange,
)

SwiftUI's Gauge in its three forms: linearCapacity (the default, a capsule track filled to the value), accessoryCircular (an open ring with a dot marking the value) and accessoryCircularCapacity (a ring filled to the value). tint colours the fill (by default blue on the linear gauge and the label colour on the rings, as SwiftUI), and the value does not animate; SwiftUI's gauges do not either. Geometry measured against SwiftUI on iOS 26.4. On Android it is a Material 3 LinearProgressIndicator or CircularProgressIndicator with the labels around it.

Sheet

showGlassSheet<void>(
  context: context,
  detents: const [GlassSheetDetent.medium, GlassSheetDetent.large],
  builder: (context) => details,
);

The sheet works like SwiftUI's presentationDetents:

  • It follows a drag between its detents, and a drag or fling below the lowest one dismisses it.
  • At a partial detent it is floating glass.
  • At large it runs edge to edge, opaque, with the screen's own corners.
  • GlassSheetDetent.fraction(0.6) and .height(400) make custom detents.

Full-screen cover

final cover = showGlassFullScreenCover<void>(
  context: context,
  builder: (context) => Column(
    children: [
      Align(
        alignment: AlignmentDirectional.topEnd,
        child: Padding(
          padding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
          child: GlassButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Done'),
          ),
        ),
      ),
      const Expanded(child: PlayerView()),
    ],
  ),
);
// later: cover.dismiss();

SwiftUI's .fullScreenCover: an opaque page sliding up over the whole screen, for a video player or a photo editor. The page below neither scales nor dims; the background runs edge to edge while the content respects the safe area, and with Reduce Motion it cross-fades instead of sliding. Navigator.pop from inside closes it, or keep the returned GlassFullScreenCoverHandle (dismiss, and result completes with the value it closed with) to close it from anywhere. Layout matches SwiftUI, and the slide's timing is fitted from a recording of SwiftUI's cover. On Android it is a Material 3 full-screen dialog.

Two options SwiftUI's cover lacks, both off by default: dragToDismiss: true lets it be dragged down to close like a sheet, and showsCloseButton: true adds a glass xmark close button top-trailing (a Material close icon on Android).

GlassMenuButton(
  icon: CupertinoIcons.ellipsis,
  semanticLabel: 'More',
  items: [
    GlassMenuItem(label: 'Copy', icon: CupertinoIcons.doc_on_doc,
        onSelected: copy),
    GlassMenuItem(label: 'Delete', icon: CupertinoIcons.trash,
        destructive: true, onSelected: delete),
  ],
)

The menu opens over the button, from its top corner, as SwiftUI's Menu does; measured from SwiftUI. GlassMenuController drives it from an outer gesture — iOS's slide-to-select under a long-press:

final controller = GlassMenuController();

GestureDetector(
  onLongPressStart: (_) => controller.open(),
  onLongPressMoveUpdate: (details) =>
      controller.glideTo(details.globalPosition),
  onLongPressEnd: (_) => controller.endGlide(),
  onLongPressCancel: controller.cancelGlide,
  child: const Text('Hold, then slide'),
)

GlassMenuButton(controller: controller, /* ... */)

On Android it is a Material 3 MenuAnchor; gliding is a no-op there, but opening and closing still work.

Context menu

GlassContextMenu(
  items: [GlassMenuItem(label: 'Copy', icon: CupertinoIcons.doc_on_doc,
      onSelected: copy)],
  child: photo,
)

A long-press lifts the item over the blurred page and opens the glass menu beside it. Assistive tech gets the items as custom actions. On Android the long-press opens a Material popup menu.

Popover and picker

GlassPopoverAnchor(
  popoverBuilder: (_) => const Padding(
    padding: EdgeInsets.all(16),
    child: Text('Liquid Glass popover'),
  ),
  builder: (context, open) => GlassButton.icon(
    onPressed: open, icon: CupertinoIcons.info, semanticLabel: 'Info'),
)

GlassPicker<Period>(
  items: const [
    GlassPickerItem(value: Period.day, label: 'Day'),
    GlassPickerItem(value: Period.week, label: 'Week'),
  ],
  selected: period,
  onChanged: (p) => setState(() => period = p),
)
  • Popover: the glass bubble opens over its button, and the button steps aside while it's open, as in SwiftUI. showGlassPopover anchors one to any widget.
  • Picker: SwiftUI's menu style. It opens the glass menu with the current choice checked.

Date picker

GlassDatePicker(
  value: date,
  firstDate: DateTime(2020),
  lastDate: DateTime(2030),
  onChanged: (d) => setState(() => date = d),
)

The compact style: the date in a grey capsule, opening a glass calendar popover. Picking a day closes it. On Android, a text button opening Material's showDatePicker.

Alert and confirmation dialog

showGlassAlert(
  context: context,
  title: 'Delete photo?',
  message: 'This photo will be deleted from all your devices.',
  actions: [
    const GlassDialogAction(label: 'Cancel', role: GlassButtonRole.cancel),
    GlassDialogAction(
      label: 'Delete',
      role: GlassButtonRole.destructive,
      onPressed: delete,
    ),
  ],
);

showGlassConfirmationDialog(
  context: context,
  title: 'Photo',
  actions: [
    GlassDialogAction(label: 'Share', onPressed: share),
    const GlassDialogAction(label: 'Cancel', role: GlassButtonRole.cancel),
  ],
);

The alert is a 320-pt glass card centred in the safe area, with its buttons side by side. The confirmation dialog is a 240-pt card with its buttons stacked. As in iOS 26, its cancel action isn't drawn: a tap outside takes it. On Android they are AlertDialogs.

Action sheet

showGlassActionSheet(
  context: context,
  title: 'Photo',
  message: 'What would you like to do with it?',
  actions: [
    GlassDialogAction(label: 'Share', onPressed: share),
    GlassDialogAction(
      label: 'Delete',
      role: GlassButtonRole.destructive,
      onPressed: delete,
    ),
  ],
  cancel: const GlassDialogAction(
    label: 'Cancel',
    role: GlassButtonRole.cancel,
  ),
);

iOS 26's confirmation dialog as SwiftUI draws it on iPhone, measured: a 240-pt glass card with a semibold title, a secondary message and the actions stacked as capsule buttons. As on iOS, cancel is not drawn — a tap outside takes it. On Android it is a Material 3 modal bottom sheet with Cancel included.

Toast

showGlassToast(
  context,
  message: 'Upload complete',
  icon: CupertinoIcons.checkmark_circle,
  action: GlassToastAction(label: 'View', onPressed: openFile),
);

A glass capsule slides in from the top with a spring; swipe up to dismiss, or wait for duration (4 s by default; null keeps it until dismissed). One toast shows at a time — later calls wait in a queue, and the returned GlassToastHandle dismisses its toast early, shown or still waiting (handle.dismiss(), await handle.closed). With Reduce Motion it fades instead of sliding, and it is announced as a live region. On Android it is a SnackBar through the nearest ScaffoldMessenger, otherwise a Material 3 snackbar-styled surface at the bottom of the screen.

Swipe actions

GlassSwipeActions(
  key: ValueKey(item.id),
  leading: [GlassSwipeAction(icon: CupertinoIcons.pin_fill, label: 'Pin',
      color: CupertinoColors.systemOrange, onPressed: () => pin(item))],
  trailing: [GlassSwipeAction(icon: CupertinoIcons.trash, label: 'Delete',
      color: GlassSystemColors.red, onPressed: () => delete(item))],
  child: ItemRow(item),
)

Swiping a row aside reveals tinted glass capsules with their labels underneath:

  • The row rubber-bands past its actions and springs open or shut.
  • A full swipe (allowsFullSwipe, the default) runs the outermost action, with a haptic as it passes the threshold.
  • Only one row is open at a time. A scroll or a tap closes it.
  • VoiceOver and TalkBack get the actions as custom actions.

iOS 26 colours

GlassSystemColors holds iOS 26's red, blue, orange and green. Flutter's CupertinoColors still has the iOS 18 values.

Component fidelity

Every component is measured against its SwiftUI counterpart on the iOS 26.4 simulator, using reference scenes in example/ios/Runner/ControlScenes.swift:

  • tool/reference/measure_components.py reads the geometry and colours into controls.json, and test/reference/ pins the Dart constants to it.
  • A Flutter twin of each scene (-twin <scene>) is scored per component by tool/reference/compare_components.py, using the fidelity harness's measures.

Over 24 component views:

path pass median SSIM median dE
native glass 9/24 0.960 1.11
shader glass 8/24 0.960 1.14

Passing views: the toggles, slider, toolbar, sheets, stepper and picker menu. The remaining gaps:

  • SF Symbols' weights against CupertinoIcons.
  • UIKit's text rendering against Flutter's.
  • The popover and menu glass tone.
  • The calendar's first weekday, which follows the region.

GlassChip, GlassBadge, GlassPageControl and GlassProgressIndicator are geometry-estimated, not yet measured.

Adaptive foreground over busy content

By default the label colour follows the theme's brightness. To follow the content actually behind the glass, wrap that content in a GlassBackdropSource; each group then samples it (a small GPU readback every 250 ms) and GlassForeground.labelColorOf(context) switches between dark and light labels.

Stack(
  children: [
    GlassBackdropSource(child: photoGrid),
    Align(
      alignment: AlignmentDirectional.bottomCenter,
      child: const Text('Now playing').glassEffect(),
    ),
  ],
)

RTL and accessibility

  • RTL: every component lays out by the reading direction — insets are directional (EdgeInsetsDirectional), the tab bar, segmented control, slider fill, list chevrons, swipe-action edges and back button all follow Directionality. The glass light angle is not mirrored, matching iOS.
  • Screen readers: one semantics node per control; an explicit semanticLabel replaces the visible text where one exists (GlassButton, GlassMenuButton, GlassMenuItem, GlassPicker, GlassSearchField, GlassTextField, showGlassPopover, and others). Tabs, segments and rows are selectable buttons; swipe actions and context menus appear as custom actions; toasts are live regions; tapping outside a menu or picker is announced as "Dismiss".
  • Reduce Motion: springs are dropped — the tab bar's lens and pill, the toggle thumb, the sheet, the toast (fades instead of sliding) and the progress indicator's easing all move or appear without animating.
  • Keyboard: interactive glass takes focus and shows a focus ring; onPressed glass and buttons answer Enter and Space.
  • Reduce Transparency (iOS) turns glass into an opaque surface, as iOS does (on iOS 26+ SwiftUI's glass does this itself).
  • Increase Contrast strengthens the rim and reduces lensing on the shader path; on iOS 26+ it is whatever SwiftUI's own glass does.

Known limitations

  • Context menu not measured: synthetic touches on the simulator do not open SwiftUI's .contextMenu, so GlassContextMenu follows the system's look but has not been measured. Springs and press animations of the newer components are not measured either.
  • Native mode, runtime light→dark flip: on iOS 26, 71 of 72 measured brightness flips at runtime match SwiftUI. One does not: a regular glass capsule over a photo, flipped from light to dark, keeps part of Apple's previous look (SSIM 0.84 against a dark launch, which matches). Apple's glass appears to keep state from glass drawn earlier in the process; it is not fixed by rebuilding the glass.
  • Native interactive glass: pressing native glass gets this package's stretch and growth, but not SwiftUI's own touch glow.
  • Shader glow: the shader's touch glow is brighter than SwiftUI's.

License

MIT — see LICENSE. The package includes code adapted from liquid_glass_widgets (MIT); see THIRD_PARTY_NOTICES.

Libraries

adaptive_liquid_glass
iOS 26 Liquid Glass for Flutter with Material 3 counterparts on Android.
testing
Fidelity-tuning hooks. Not part of the stable API.