All Articles
CategorySwiftUI
Reading Time
9 min read
Published
2026-10-15
Word Count
1,598words

Grab a coffee — this one is a deep dive!

LiquidGlassKit: one glass modifier for OS 26 and the systems before it

Summary

How LiquidGlassKit draws native Liquid Glass on OS 26 and a Material fallback down to iOS 17, and how its render tests were made to fail when they should.

  • SwiftUI's Liquid Glass API, glassEffect, exists only on iOS, macOS, tvOS and watchOS 26.
  • Only the Surface folder calls SwiftUI's glass API: the glassSurface(_:in:interactive:) modifier and GlassGroup, which wraps GlassEffectContainer on the native path.
  • Add the package in Xcode with File › Add Package Dependencies… and the URL https://github.com/muhittincamdali/LiquidGlassKit.git, or in Package.swift.
  • The current release is 2.1.0, and the README says no later release is scheduled yet.
LiquidGlassKit: one glass modifier for OS 26 and the systems before it

Table of Contents


The problem

SwiftUI's Liquid Glass API, glassEffect, exists only on iOS, macOS, tvOS and watchOS 26. An app

whose users are still partly on iOS 17 or 18 has two options: ship the glass look only to OS 26

users, or wrap every glass view in an if #available(iOS 26, *) branch with a hand-made

alternative in the else. The second option spreads the same decision over every screen, and

each copy tends to drift: one card forgets the theme, another ignores Reduce Transparency.

LiquidGlassKit moves that decision into one place. On OS 26 it draws with SwiftUI's native

glassEffect; on earlier versions (back to iOS 17, macOS 14, tvOS 17 and watchOS 10) and on

visionOS it draws a Material blur with a tint, an edge highlight and a shadow. You write the same

code for every OS. The package has no third-party dependencies and is distributed through Swift

Package Manager only.

Design decisions and alternatives

One entry point. Only the Surface folder calls SwiftUI's glass API: the

glassSurface(_:in:interactive:) modifier and GlassGroup, which wraps GlassEffectContainer on

the native path. Every component (buttons, cards, text fields, the search bar, the tab bar, the

navigation bar, the sheet, and since 2.1.0 the switch and the slider) is drawn through

glassSurface. The alternative was a separate #available branch per component. The February

2026 code had no shared path: each component filled its own Material

(.fill(material.swiftUIMaterial)), and the 2.0.0 changelog records the result: components that

ignored the theme. The cost of the single entry point is that a component-specific look, such as a different

shadow for buttons, needs a new parameter on the shared modifier.

Staying out of SwiftUI's namespace. The February code defined its own glassEffect(_:) and

GlassEffectContainer. Against the OS 26 SDK those names collide with SwiftUI's, and a file that

imports both modules gets ambiguous calls. The rebuilt API uses glassSurface, GlassGroup and

LiquidGlassButtonStyle (SwiftUI already has a GlassButtonStyle). Users learn the package's names

instead of Apple's; the README and the DocC catalog name the SwiftUI API each one wraps.

Choosing the rendering path in three layers. #if os(visionOS) removes the native call at

compile time, because SwiftUI marks glassEffect unavailable there. A runtime

#available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) check lets one binary run on iOS 17 and

iOS 26. On top of that, GlassRenderingMode (automatic or fallback) comes from the

environment, so an app can force the fallback for a consistent look. The decision itself is a pure

function, resolved(nativeAvailable:), and all four combinations are tested. An early design also

had a .native case; it was removed before 2.0.0 (6293c16) because it always behaved like

.automatic: native glass cannot be forced where it does not exist, and where it exists

.automatic already uses it.

An enum for shapes. GlassShape is an enum (rounded rectangle, capsule, circle, per-corner

radii) rather than a generic Shape, so it can live inside GlassTheme, be Hashable in tests

and be Sendable under Swift 6. Drawing goes through AnyShape. The trade-off is real: you cannot

pass your own shape.

Styles instead of custom views. Buttons are a ButtonStyle (.buttonStyle(.liquidGlass)) and

the switch is a ToggleStyle (.toggleStyle(.liquidGlass)). Pressed state comes from

configuration.isPressed, and keyboard and VoiceOver behavior stays that of a normal SwiftUI

Button or Toggle. GlassButton remains as a convenience view that uses the same style.

Merging by rewriting. Version 2.1.0 takes ideas from two earlier repositories by the same

author, LiquidGlass-Playground and GlassmorphismUI, both MIT licensed. Their code drew its own

Material layers and depended on UIKit for haptics and accessibility checks. Instead of copying

files, each feature (switch, slider, gradient glass, highlights, presets, the code exporter, tab

badges) was rewritten on top of glassSurface, so it picks up the theme, the rendering mode and

Reduce Transparency. Some features were left out on purpose: blur radius, refraction and

saturation values have no counterpart in native glass. `swift package

diagnose-api-breaking-changes v2.0.0` reports no breaking changes, so the release is a minor

version. Both source repositories are now archived and point here.

What went wrong: green checks that tested nothing

The repository's history is not a straight line, and the changelog says so. The v1.0.0 tag from

February 2026 points at the first commit, which does not build. In June 2026 most of the source was

replaced by placeholders, the test target referenced types that did not exist, and CI still showed

green because the test and lint steps had continue-on-error. Version 2.0.0 rebuilt the package

and removed that flag; no workflow in the repository uses it today.

The more useful lesson came after the rebuild, when the new tests were already green. Removing

features one at a time from the source showed that the suite did not notice. Commit 36ef829

describes it: the sheet test only compared the image width with the frame width, which the frame

sets anyway, and removing the glass from GlassCard or GlassTabBar, ignoring a card's material,

or drawing dark button labels in black again all left the suite green. The tests were rewritten to

count pixels. Views are drawn on a white canvas with a green theme, and every component must draw

at least 250 green pixels on the fallback path; an explicit blue material must win over the green

theme. Two ImageRenderer quirks shaped the design: on macOS 27 a view that draws nothing can

return a buffer holding an earlier render, so transparency alone is not a signal; and text fields

render as a red and yellow placeholder, so red was not usable as a test tint. After the change,

each of 11 source mutations listed in the commit fails at least one test.

The native path had the same blind spot. On OS 26, the render test only required an image. With

the native view changed to always draw its fallback, all 39 tests still passed. Since

ImageRenderer does not capture the blur and draws native glass differently per OS, a tint check

could not work everywhere. Commit 29ce746 renders each component on both paths instead: two

fallback renders of the same view are pixel-identical, and a native render must differ from the

fallback in more than 250 pixels. The commit records at least 3,857 differing pixels per component

on macOS 27 and 5,005 on the iOS 26.5 and 27 simulators.

One more problem appeared during 2.1.0. Pixel-equality tests failed in about one full run in four,

and one failed every time with swift test --no-parallel. Commit 68f94e8 found two causes:

ImageRenderer caches its image, and the first renders of a view differ from later ones while

Material edges and glyphs settle. The test helper now uses a new renderer per pass, warms up for

eight passes and stops when two consecutive passes match, with a limit of 16.

Usage

Add the package in Xcode with File › Add Package Dependencies… and the URL

https://github.com/muhittincamdali/LiquidGlassKit.git, or in Package.swift. The README's

quick start, which the test target compiles verbatim:

swift
1import LiquidGlassKit
2import SwiftUI
3 
4struct ContentView: View {
5 var body: some View {
6 ZStack {
7 LinearGradient(colors: [.indigo, .pink], startPoint: .top, endPoint: .bottom)
8 .ignoresSafeArea()
9 VStack(spacing: 16) {
10 GlassCard {
11 Label("Hello, glass", systemImage: "sparkles")
12 }
13 GlassButton("Tap me", systemImage: "hand.tap") {}
14 }
15 }
16 }
17}

Any view can sit on glass with one modifier:

swift
1import LiquidGlassKit
2import SwiftUI
3 
4struct NowPlayingBadge: View {
5 var body: some View {
6 Label("Now Playing", systemImage: "music.note")
7 .padding()
8 .glassSurface(.tinted(.purple), in: .capsule)
9 }
10}

A theme and a rendering mode apply to a whole hierarchy:

swift
1struct ThemedApp: View {
2 var body: some View {
3 RootView()
4 .glassTheme(GlassTheme(material: .tinted(.indigo), shape: .roundedRectangle(cornerRadius: 12)))
5 .glassRenderingMode(.automatic) // or .fallback to use the Material look everywhere
6 }
7}

A preset can be adjusted and exported as Swift source that uses the package:

swift
1var theme = GlassPreset.named("Ocean")?.theme ?? .standard
2theme.shadowRadius = 4
3let source = GlassCodeExporter().viewCode(named: "PromoCard", theme: theme)

The demo app in Examples/LiquidGlassDemo shows a gallery, tab bars, the sheet, a native versus

fallback comparison and a playground that tunes a preset and shows its exported code. The DocC

articles are published on GitHub Pages.

Limits

  • The final glass look is not tested. ImageRenderer does not capture the blur and captures

native glass only in part. The tests check the fallback's tint and coverage, and that the native

path renders differently from the fallback; the README states this.

  • Test coverage of devices. CI runs the suite on macOS, an iOS 26 simulator and an iOS 18.6

simulator, where only the fallback exists. There is no iOS 17 job and no run on a physical

device.

  • No performance numbers. The repository has no benchmark or profiling results for the

fallback blur and shadows.

  • visionOS uses the fallback only, because SwiftUI does not offer glassEffect there.
  • GlassSheet is an overlay, not a system sheet: no detents, no separate presentation context,

and it is not available on tvOS. GlassSlider is not available on tvOS either.

  • Fixed shapes. GlassShape has four cases; custom shapes are not supported.
  • Localization. Titles of GlassButton, GlassTab and GlassTextField are String values

shown verbatim, so no LocalizedStringKey lookup happens, and the package ships no localized

strings of its own.

  • Toolchain. Xcode 26 or later is required even for older deployment targets, because the

native path is compiled against the OS 26 SDK.

The current release is 2.1.0, and the README says no later release is scheduled yet.

Source code: github.com/muhittincamdali/LiquidGlassKit

Tags

#swiftui#liquid-glass#swift-package#testing#accessibility
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

Share