fresnel 0.1.0
fresnel: ^0.1.0 copied to clipboard
Apple Liquid Glass for Flutter. Real native iOS 26+ glass via SwiftUI platform views, with a calibrated shader fallback on Android, web and desktop.
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 callsUIGlassEffectorglassEffectat 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:
- Reduce Transparency wins over everything, including an explicit request.
- An explicit
opaqueorfallbackrequest is honoured as-is. shaderis honoured where Impeller is present, and degrades tofallbackwhere it is not.nativeandautotake native glass if the device has it, then the shader, then the fallback.
Two design decisions worth stating outright:
- SwiftUI, not UIKit.
UIGlassEffectrenders a material but has no equivalent ofglassEffectIDmorphing orglassEffectUnion. 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 |
Navigation #
| 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
