liquid_glass_ios_widgets

Native iOS 26 Liquid Glass components as Flutter widgets.

Each widget is backed by a real SwiftUI / UIKit view through a platform view (UiKitView). You get the system look and behavior: refraction, the glass thumb on sliders and switches, the press shimmer, and the floating tab bar.

Widget Native component
LiquidGlassButton / LiquidGlassButton.icon SwiftUI Button + .buttonStyle(.glass) / .glassProminent
LiquidGlassSwitch UISwitch
LiquidGlassSlider SwiftUI Slider
LiquidGlassSegmentedControl SwiftUI Picker + .pickerStyle(.segmented)
LiquidGlassContainer SwiftUI .glassEffect(_:in:), with a Flutter child on top
LiquidGlassTabBar UITabBar

Requirements

  • iOS only. The plugin registers only the iOS platform.
  • Building requires Xcode 26+ (iOS 26 SDK).
  • The iOS deployment target must be 15.0 or higher.
  • Real Liquid Glass renders on iOS 26+. On iOS 15–18 the same widgets use native materials and .bordered button styles.
  • On non-iOS platforms (e.g. widget tests) the widgets render Cupertino fallbacks.

Set the minimum iOS version in ios/Podfile:

platform :ios, '15.0'

Usage

import 'package:liquid_glass_ios_widgets/liquid_glass_ios_widgets.dart';

LiquidGlassButton(
  label: 'Share',
  systemImage: 'square.and.arrow.up', // SF Symbols name
  onPressed: () {},
);

LiquidGlassButton.icon(
  systemImage: 'plus',
  style: LiquidGlassButtonStyle.prominent,
  tint: Colors.orange,
  onPressed: () {},
);

LiquidGlassSwitch(
  value: enabled,
  onChanged: (value) => setState(() => enabled = value),
);

LiquidGlassSlider(
  value: volume,
  onChanged: (value) => setState(() => volume = value),
);

LiquidGlassSegmentedControl(
  selectedIndex: index,
  onChanged: (i) => setState(() => index = i),
  segments: const [
    LiquidGlassSegment(label: 'Day'),
    LiquidGlassSegment(label: 'Week'),
    LiquidGlassSegment(systemImage: 'calendar'),
  ],
);

LiquidGlassContainer(
  shape: const LiquidGlassShape.rect(cornerRadius: 24),
  padding: const EdgeInsets.all(16),
  child: const Text('Flutter content on native glass'),
);

The tab bar is meant to float over content, so the glass has something to refract:

Stack(
  children: [
    content,
    Positioned(
      left: 0,
      right: 0,
      bottom: MediaQuery.paddingOf(context).bottom,
      child: LiquidGlassTabBar(
        currentIndex: tab,
        onTap: (i) => setState(() => tab = i),
        items: const [
          LiquidGlassTabItem(label: 'Home', systemImage: 'house', selectedSystemImage: 'house.fill'),
          LiquidGlassTabItem(label: 'Search', systemImage: 'magnifyingglass'),
        ],
      ),
    ),
  ],
);

Checking for Liquid Glass support

LiquidGlass.isSupportLiquidGlass is a synchronous bool. It's true on iOS 26+ and false on older iOS versions and other platforms, so you can use it directly in build:

if (LiquidGlass.isSupportLiquidGlass) {
  // real Liquid Glass
}

LiquidGlassAdaptive shows its first widget when Liquid Glass is supported and its second widget otherwise:

LiquidGlassAdaptive(
  glass: LiquidGlassButton(label: 'Save', onPressed: save),
  fallback: CupertinoButton.filled(onPressed: save, child: const Text('Save')),
);

await LiquidGlass.isSupported() asks the native side for the same answer.

How it works

Flutter widget ──creationParams──▶ UiKitView ──▶ FlutterPlatformViewFactory
      ▲                                                  │
      │   MethodChannel "liquid_glass_ios_widgets/view_<id>"
      │     Dart → iOS: update(params)                   ▼
      └──── iOS → Dart: onPressed / onChanged / …   UIHostingController(SwiftUI view)
                        intrinsicSize                 or a UIKit control
  • Each native view gets its own method channel. When the widget rebuilds with new parameters, the whole parameter map is sent with update.
  • Widgets without an explicit size, such as buttons and the segmented control, are sized from the size the native side measures and reports (intrinsicSize).
  • Controls are controlled: rebuild with the new value in onChanged. If you don't, the native control snaps back to the value you passed.

Dark mode

Native views follow the Flutter theme's brightness (CupertinoTheme.brightnessOf(context), which inside a MaterialApp comes from the Material theme), not the iOS system appearance. So an in-app light/dark switch (ThemeMode) applies to the native components too.

Limitations

  • Light theme on a dark system. As of iOS 26, the Liquid Glass material follows the device's system appearance and can't be lightened per view, so with the system in dark mode and the app in a light theme, glass surfaces stay dark. The text and controls inside them follow the app theme. A dark app theme on a light system works fully.

  • Performance. Every widget is a separate platform view. Dozens of them in a scrolling list cost more than plain Flutter widgets.

  • No morphing between widgets. GlassEffectContainer merging and morphing only works inside a single SwiftUI hierarchy, so it can't happen between separate Flutter widgets.

  • The glass refracts content Flutter draws behind the platform view. Content drawn on top of it (e.g. LiquidGlassContainer.child) is not refracted.

  • Icons are SF Symbols names ('heart.fill'), not Flutter IconData.

Example

cd example && flutter run

Libraries

liquid_glass_ios_widgets
Native iOS 26 Liquid Glass components as Flutter widgets.