fresnel

Apple Liquid Glass for Flutter. Real native glass on iOS 26+, a calibrated shader everywhere else, behind one API.

GlassSurface(
  glass: Glass.regular.tinted(const Color(0xFF0A84FF)).interactive(),
  shape: const GlassShape.superellipse(28),
  child: const Text('Hello'),
)

Why this exists

Liquid Glass stops being optional. An app linked against the iOS 27 SDK gets the new material whether or not it was designed for it, and the App Store deadline for building against that SDK lands around April 2027. Every Flutter iOS app has to arrive somewhere by then, and the two things it can arrive with today are both compromises:

  • Shader packages (liquid_glass_widgets, liquid_glass_renderer) draw their own glass. That works everywhere, but it can never match iOS, because it cannot inherit Apple's refinements — including the system-wide glass intensity slider added at WWDC26, which users expect every glass surface on the device to obey.
  • Native control bridges (adaptive_platform_ui, cupertino_native) hand you real system controls, and neither calls UIGlassEffect or glassEffect at all. You get a system switch; you cannot put glass on your own surface.

Flutter's own Cupertino work is paused pending a standalone package:cupertino.

fresnel takes the third route: bridge the real material for surfaces the OS can render, and fall back to a shader calibrated against it everywhere else, so one widget tree serves both.

fresnel shader packages native control bridges Flutter Cupertino
Calls glassEffect / UIGlassEffect yes no no no
Glass on an arbitrary surface yes yes no no
Inherits OS refinements and the intensity slider yes, on iOS 26+ no for the controls they bridge no
Container merging, unions, glassEffectID morphing yes, on iOS 26+ no no no
Works off Apple platforms yes, via shader yes partly yes
Status 0.1.0 shipping shipping paused

Quickstart

dependencies:
  fresnel: ^0.1.0
import 'package:flutter/material.dart';
import 'package:fresnel/fresnel.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GlassScaffold(
        toolbar: const GlassToolbar(title: Text('Library'), largeTitle: true),
        body: ListView.builder(
          itemCount: 40,
          itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
        ),
      ),
    );
  }
}

Fresnel.ensureInitialized resolves the device's capabilities and warms the shader. Surfaces render before it completes — they simply take the shader path until it lands — so awaiting it in main only stops the swap being visible on the first frame. fresnel itself depends on neither Material nor Cupertino; the app above uses MaterialApp because most apps already have one.

Render paths

GlassSurface dispatches on capabilities resolved once at startup. There are four paths and one widget; nothing in the tree changes shape between them.

Path When What it is
Native iOS 26+ SwiftUI .glassEffect() in a UIHostingController, hosted in a UiKitView. Inherits every OS refinement, including the intensity preference.
Shader Impeller, no native glass fresnel_glass.frag: SDF-driven edge refraction and a specular rim, composed over a hardware gaussian.
Fallback Skia, web, or a shader that failed to compile BackdropFilter with a tint and a hairline rim.
Opaque Reduce Transparency A solid fill. Not a thinner blur — see Accessibility.

Resolution order, in FresnelScope.resolve:

  1. Reduce Transparency wins over everything, including an explicit request.
  2. An explicit opaque or fallback request is honoured as-is.
  3. shader is honoured where Impeller is present, and degrades to fallback where it is not.
  4. native and auto take native glass if the device has it, then the shader, then the fallback.

Two design decisions worth stating outright:

  • SwiftUI, not UIKit. UIGlassEffect renders a material but has no equivalent of glassEffectID morphing or glassEffectUnion. Bridging SwiftUI is the only route to the full material.
  • The platform view renders only the material. Content stays an ordinary Flutter subtree composited above it, and the native view takes no touches. Text, gestures, navigation, theming and accessibility never cross the FFI boundary — which is how this avoids the navigation- and state-corruption failures that make comparable native bridges unsafe for production.

FresnelScope: one path per route

Wrap a screen to fix its render path:

FresnelScope(
  path: GlassRenderPath.shader,
  child: MyPage(),
)

The scope is not a convenience for per-widget overrides, and there is no per-widget flag. A Flutter BackdropFilter painted in the same scene as a native glass platform view makes the platform view stop rendering entirely — no error, no log, no fallback, just an invisible surface. See KNOWN_ISSUES.md issue #1 for the reproduction and what is and is not established about the cause.

Because the shader and fallback paths are BackdropFilters and the native path is a platform view, mixing them in one scene is exactly that broken combination. Scoping the choice to a subtree means it cannot be expressed: a normal app resolves one path globally, and an app that overrides does so for a whole route.

FresnelScope also carries an optional theme, so one widget configures a screen:

FresnelScope(
  path: GlassRenderPath.fallback,
  theme: FresnelThemeData.dark().copyWith(blurSigma: 20),
  child: MyPage(),
)

Component catalogue

Everything below is exported from package:fresnel/fresnel.dart. Nothing here wraps Navigator or the root view controller, and no component talks to a platform channel — they compose GlassSurface, GlassContainer and GlassElementView, which is what keeps them correct on all four paths.

Core

Dart SwiftUI
Fresnel — capability handshake and shader warm-up
GlassSurface .glassEffect(_:in:)
GlassContainer GlassEffectContainer(spacing:)
GlassElementView .glassEffectID(_:in:), .glassEffectUnion(id:namespace:)
Glass, GlassVariant Glass.regular / .clear / .identity, .tint(_:), .interactive(_:)
GlassShape Capsule, Circle, RoundedRectangle, ConcentricRectangle
GlassSymbol, SymbolWeight Image(systemName:), UIImage.SymbolWeight
GlassCapabilities — resolved device facts
FresnelScope, GlassRenderPath, GlassRenderPathX, FresnelScopeTheme
FresnelTheme, FresnelThemeData

Controls

Dart SwiftUI
GlassButton, GlassButtonStyle, GlassButtonShape .buttonStyle(.glass) / .glassProminent, .buttonBorderShape(_:)
GlassIconButton a Button with a symbol label, .glass
GlassControlSize .controlSize(_:)
GlassControlSymbol Image(systemName:)
GlassSwitch Toggle
GlassSlider Slider
GlassStepper Stepper
GlassSegmentedControl, GlassSegment Picker with .segmented
GlassProgressIndicator, GlassProgressStyle ProgressView, .linear / .circular
GlassBadge .badge(_:)
GlassChip no direct counterpart; closest is a Toggle with .button style
Dart SwiftUI
GlassScaffold — the full-bleed layout .toolbar and TabView produce implicitly
GlassToolbar, GlassToolbarItem .toolbar { ToolbarItem { … } }
ToolbarSpacer ToolbarSpacer
SharedBackgroundVisibility .sharedBackgroundVisibility(_:)
GlassTabBar, GlassTab TabView, Tab
GlassTabBarMinimizeBehavior .tabBarMinimizeBehavior(_:)
GlassTabBarAccessory, GlassTabAccessoryPlacement, GlassTabAccessoryBuilder .tabViewBottomAccessory, TabViewBottomAccessoryPlacement
GlassSearchField, GlassSearchMinimizeBehavior .searchable, .searchToolbarBehavior(_:)
GlassSidebar, GlassSidebarItem NavigationSplitView's sidebar column
GlassNavigationRail, GlassRailDestination the rail TabView renders at medium width
GlassScrollScope, GlassScrollState — scroll telemetry the chrome reacts to

Overlays

Everything modal here is a Navigator route, so the system back gesture, barrier taps and Navigator.pop work with nothing wired up, and every show* function completes with the value the route was popped with. showGlassToast is the exception — an overlay entry, because a transient notification must not enter the navigation history.

Dart SwiftUI / UIKit
showGlassSheet, GlassSheet, GlassSheetDetent .sheet with .presentationDetents, UISheetPresentationController.Detent
showGlassDialog, GlassDialog, GlassDialogAction .alert
showGlassActionSheet, GlassActionSheetAction .confirmationDialog
GlassMenu, GlassMenuEntry, GlassMenuItem, GlassMenuDivider, GlassMenuSection, GlassMenuTrigger Menu, .contextMenu
showGlassPopover, GlassPopover, GlassPopoverSide .popover
showGlassToast, GlassToast, GlassToastAlignment no counterpart — iOS has no toast

Expressive

Glass widgets Apple does not ship. Everything above is a Flutter counterpart of something in SwiftUI; this group has none by definition. Each carries a note in its own docs on when reaching for it is defensible and when it is not — several are content-layer glass, which the HIG argues against.

Dart What it is
GlassCard a panel on the content layer. Off-spec on purpose; read the docs before using it
GlassPanel a large surface for a sidebar, inspector or detail pane
GlassListSection the iOS inset-grouped list with the group fill replaced by glass
GlassDock, GlassDockItem a fused bar of items that magnify under the pointer
LiquidBlob, LiquidBlobSpec free-form shapes that fuse and tear apart as they move
GlassUnionShape one shape in a merged group
GlassProgressRing a determinate arc on a glass disc
GlassSpecular drives the light angle from the device's motion
GlassPlayground, GlassPlaygroundValue, GlassPlaygroundShape tunes the shader live and emits the Dart for what you tuned

Lower-level building blocks

package:fresnel/primitives.dart exports FresnelPlatform, ShaderGlassSurface, FallbackGlassSurface, GlassShaderProgram, sdfRadiusFor and shapeBorderFor, for people writing their own glass widgets. It is separate so the escape hatch does not clutter autocomplete for the 95% case.

Theming

FresnelThemeData is the token set every glass surface in a subtree reads. Defaults are calibrated against iOS 26's system materials, so a surface with no configuration looks right beside native UI — override selectively rather than wholesale.

FresnelTheme(
  data: FresnelThemeData.dark().copyWith(
    accent: const Color(0xFF30D158),
    defaultShape: const GlassShape.superellipse(26),
    blurSigma: 18,
  ),
  child: MyPage(),
)
Token group Members Applies to
Colour roles onGlass, onGlassSecondary, opaqueFallback, accent, separator every path
Geometry defaultShape, containerSpacing every path
Material physics blurSigma, thickness, refraction, lightAngle shader and fallback only — the OS owns the material on the native path
Motion morphDuration, morphCurve, pressDuration, pressScale every path

With no FresnelTheme above them, widgets derive light or dark tokens from the ambient MediaQuery brightness, so glass is correct with no setup and follows the system when the user switches appearance.

Accessibility

Setting Behaviour
Reduce Transparency Every path collapses to an opaque fill in FresnelThemeData.opaqueFallback. Not a thinner blur — the setting means the user does not want translucency, and honouring it halfway honours it not at all. This overrides an explicit FresnelScope path.
Reduce Motion Morphs, press responses, tab-bar minimisation, toast entrances and the specular sweep are suppressed. The state change still happens; only the tween is removed. Indeterminate progress publishes a live semantic value instead, so the information survives the loss of the animation.
Increase Contrast Reported in GlassCapabilities.increaseContrast and available to your own widgets. Nothing in fresnel reads it yet — on the native path the OS adjusts the material itself, and the shader path's equivalent is not written. Treat this row as a gap, not a feature.

Both Flutter's own MediaQueryData.disableAnimations and the OS accessibility flag resolved at startup are honoured — the latter is the only signal available on platforms with no native implementation.

Controls carry semantics rather than relying on their visuals: GlassIconButton requires a semanticLabel because it has no visible text, GlassChip publishes a selected state, and sliders and steppers publish values.

Platform support

Platform Path today Notes
iOS 26+ native fresnel_ios bridges SwiftUI glassEffect
iOS 13–25 shader one binary serves both; glass call sites are gated with #available(iOS 26.0, *)
Android shader falls to fallback without Impeller
macOS shader macOS 26 has native Liquid Glass; a fresnel_macos implementation is not published yet
Windows, Linux shader
Web fallback backdrop shaders are unavailable, so BackdropFilter with a tint

Requires Flutter 3.32 or newer, for RoundedSuperellipseBorder — the Apple squircle, which is the shape iOS actually uses. Web needs 3.38.3.

The package is federated, so each platform's implementation is independently versioned and an app only pays for what it uses:

Package Role
fresnel façade — what apps depend on
fresnel_platform_interface the contract
fresnel_shader pure-Dart shader fallback
fresnel_ios SwiftUI glassEffect bridge

Status

0.1.0. Verified on iPhone 17 Pro simulator, iOS 26.1, Flutter 3.44.8, Xcode 26.1.1: native SwiftUI glass samples and refracts Flutter-rendered content beneath it, which is the question the whole architecture rests on.

One confirmed engine-level issue, and it is the reason the render path is route-scoped rather than per-widget: see KNOWN_ISSUES.md.

Contributing

See CONTRIBUTING.md for the federated layout, how to add a platform implementation, and the SDK-verification rule.

License

MIT

Libraries

fresnel
Apple Liquid Glass for Flutter.
primitives
Lower-level building blocks for people writing their own glass widgets.