Liquid Glass in iOS 26: Adapting a SwiftUI App to Apple's New Design Language#
When Liquid Glass was unveiled at WWDC 2025, the first reaction of most iOS developers wasn't "how does this work" — it was "how many screens do I now have to redo." That's a fair question. Liquid Glass isn't a cosmetic reskin like the flat design of iOS 7; it's a material with its own physics. It refracts light, reacts to device motion and touch, blends with neighboring elements, and morphs between states. SwiftUI ships a dedicated API surface for it, and if you bolt it onto an old UI purely mechanically, your app ends up looking foreign next to system screens.
I've adapted several of my own apps to Liquid Glass, including MeteoHealth, and this article collects what actually mattered: the current API as of 2026 (with the iOS 26.1 and 26.2 refinements), the mistakes that keep recurring when retrofitting custom UI, and what happens to your app once a user turns on Reduce Transparency.
What Liquid Glass Actually Is — Not Just Another Blur#
Before iOS 26, we had materials — .ultraThinMaterial, .regularMaterial, and the rest of the Material family — a static blur with configurable opacity. Liquid Glass isn't an evolution of materials; it's a separate system layer that:
- refracts and reflects the content underneath instead of simply blurring it;
- reacts to touch and pointer input — an element gets a slight spring and highlight on press;
- merges with neighboring glass elements when they come close, and morphs between shapes as the view hierarchy changes;
- adapts automatically to what's behind it — a light background gets a more contrasty edge, a dark one a more transparent look.
Apple's key architectural rule: Liquid Glass is a material for the functional layer — navigation, toolbars, controls, transient overlays — not the content layer. An article card, a photo in a gallery, a message list — that's content, and turning it into glass isn't a good idea even where it's technically possible. The second rule is: never stack glass on glass. If an element already sits on a system navigation bar or sheet, an extra glassEffect on top produces a muddy blur instead of the depth you were after.
The New API: glassEffect, Glass, and Modifier Order#
The basic entry point is the .glassEffect() modifier. Without parameters, it wraps a view in a Capsule shape with the .regular variant:
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect()You can specify a shape explicitly (.rect(cornerRadius:), .circle, .capsule), and the material's behavior is configured through the Glass struct: .regular is the base glass, .tint(Color) adds a color accent for more prominent elements, and .interactive() turns on a spring-like reaction to taps:
struct WeatherSummaryCard: View {
let temperature: String
let condition: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(condition)
.font(.headline)
Text(temperature)
.font(.system(size: 34, weight: .semibold, design: .rounded))
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
.modifier(AdaptiveGlassBackground(cornerRadius: 24))
}
}
/// Applies Liquid Glass on iOS 26+, falls back to Material on older systems.
/// Note the order: layout modifiers (padding, frame) come first,
/// the glass effect is applied last, on top of the finished layout.
struct AdaptiveGlassBackground: ViewModifier {
let cornerRadius: CGFloat
func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
content.glassEffect(
.regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}Modifier order matters here. glassEffect needs to come after the modifiers that define layout and appearance (padding, frame, font), not before — the material measures the view's final bounds and needs the layout already finished. Putting .glassEffect() before .padding() is a common reason developers see "the glass getting clipped at the wrong edge."
GlassEffectContainer, Morphing, and glassEffectID: Dynamic States#
Once you have more than one glass element sitting near each other — a quick-actions bar, say — wrap them in a GlassEffectContainer. The container establishes a shared sampling region and lets neighboring elements visually blend instead of just stacking:
enum QuickAction: String, CaseIterable, Identifiable {
case refresh, share, favorite
var id: String { rawValue }
var symbolName: String {
switch self {
case .refresh: return "arrow.clockwise"
case .share: return "square.and.arrow.up"
case .favorite: return "heart"
}
}
}
struct QuickActionsBar: View {
@State private var isExpanded = false
@Namespace private var glassNamespace
var body: some View {
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
isExpanded.toggle()
}
} label: {
Image(systemName: isExpanded ? "xmark" : "plus")
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID("toggle", in: glassNamespace)
if isExpanded {
ForEach(QuickAction.allCases) { action in
Button {
// handle action
} label: {
Image(systemName: action.symbolName)
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID(action.id, in: glassNamespace)
.glassEffectUnion(id: "expanded", namespace: glassNamespace)
}
}
}
}
}
}Two details matter here. First, the spacing value on GlassEffectContainer controls how close neighboring elements need to be before they start visually merging — smaller values mean the views must sit closer together. Second, glassEffectID paired with @Namespace is what turns buttons appearing and disappearing into a smooth morph instead of a hard crossfade — without withAnimation wrapping the isExpanded change, the effect simply won't trigger. glassEffectUnion additionally groups elements into a single glass surface when they're created outside a shared HStack — for example, inside a dynamic ForEach.
Fallbacks for Pre-iOS 26: Two Design Languages, One App#
If your app still has users on iOS versions older than 26 — and most real-world products will for another year or two — a fallback isn't optional, it's a required part of adoption. The right pattern is to avoid maintaining two parallel view trees and instead wrap the decision in a single modifier gated by #available, as in the AdaptiveGlassBackground example above: .glassEffect(...) on iOS 26+ and .background(.ultraThinMaterial, in:) on older systems, using the same shape (RoundedRectangle vs. .rect(cornerRadius:)) so the card's geometry doesn't shift between OS versions.
A common mistake at this step is forgetting that .glassEffect() has an isEnabled parameter, which is more convenient than an if #available check buried inside the view — it lets you keep a single view implementation and toggle the material with a flag computed once, higher up the tree.
Common Mistakes When Retrofitting Custom UI#
Across several app migrations, the same mistakes keep showing up:
- Glass on glass. A custom card using
glassEffectinside a systemsheetorNavigationStackthat already has a glass toolbar turns into an opaque gray smear — contrast and text legibility drop. - Glass on the content layer. An article cover, a profile photo, a player background — that's content, not a functional element; dressing content up with glass usually makes an interface less readable, not more premium.
- Multiple independent glass elements with no container. Each icon gets its own
glassEffect()with noGlassEffectContainer— rendering gets more expensive, and the visual merging you were going for never happens. - Wrong modifier order.
glassEffectbeforepaddingorframe— the material measures bounds against an unfinished layout. - Ignoring Reduce Transparency. Code that only checks
#available(iOS 26, *)and nothing else ignores the fact that a chunk of your users deliberately reduce interface transparency.
Accessibility: Reduce Transparency, Tinted Mode, and What You Can't Skip#
Starting with iOS 26.1, Apple gave users direct control over Liquid Glass: a Reduce Transparency toggle under Accessibility → Display & Text Size increases the material's opacity, and Settings → Display & Brightness → Liquid Glass got a switch between "Clear" and "Tinted" modes — the latter raises contrast through a subtle darkening. iOS 26.2 added a transparency slider for the Lock Screen clock on top of that.
For developers, this means you don't need to manually disable glassEffect when accessibilityReduceTransparency is set — the system already increases the material's opacity on its own. What you do need to handle yourself is that environment value where you're deciding whether to add .interactive(), since the spring-like animation is a separate accessibility concern from transparency:
struct AccessibleGlassSurface<Content: View>: View {
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
let cornerRadius: CGFloat
@ViewBuilder var content: Content
var body: some View {
if #available(iOS 26.0, *) {
content
// Don't disable glassEffect manually when Reduce Transparency
// is on — iOS already increases frosting for the .regular
// variant. We only drop the extra .interactive() bounce,
// since motion is a separate accessibility concern.
.glassEffect(
reduceTransparency ? .regular : .regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}Test this in previews, not just by flipping the system setting back and forth on a device: .environment(\.accessibilityReduceTransparency, true) in a #Preview saves a lot of time.
API Matrix: What to Change During Adoption#
| UI element | Before iOS 26 | iOS 26 (Liquid Glass) |
|---|---|---|
| Card / panel | .background(.ultraThinMaterial, in: RoundedRectangle(...)) | .glassEffect(.regular, in: .rect(cornerRadius:)) |
| Action button | Custom ZStack with manual blur and shadow | .buttonStyle(.glass) / .buttonStyle(.glassProminent) |
| Group of nearby icons | Individual material on each one | GlassEffectContainer + glassEffectUnion |
| Element appearing/disappearing | Plain .transition, unaware of neighbors | glassEffectID + @Namespace for morphing |
| Toolbar / tab bar accessory | Custom background, manual group separation | Automatic glass surface + ToolbarSpacer |
How This Played Out in Practice: MeteoHealth and an Adoption Checklist#
In MeteoHealth, the elements that actually became glass were the functional ones: a quick-actions bar on the home screen, floating controls over wellbeing charts, and a tab bar accessory showing the current weather summary — everything that sits "above" the content rather than being content. Forecast cards and history charts stayed on a plain background: testing with real users quickly showed that glass over dense numeric data hurt legibility rather than making the interface feel nicer.
The final pre-release checklist:
- Glass is applied only to the functional layer — navigation, toolbars, transient controls
- No "glass on glass" cases (checked against real sheets/NavigationStacks)
- Neighboring glass elements are wrapped in a
GlassEffectContainer -
glassEffectcomes after layout modifiers, not before - There's an
#available(iOS 26, *)fallback toMaterialwith matching shape geometry - Behavior with
accessibilityReduceTransparencyis verified in previews and on device - Tinted mode under Settings → Display & Brightness → Liquid Glass has been checked
- Morphing between states is wrapped in
withAnimation
Liquid Glass is a rare case of Apple shipping not a coat of paint but a material with real physics and its own composition rules. Copying old Material blurs one-to-one onto glassEffect works fine right up to the first screen where two glass elements — or a glass element and a system component — end up next to each other. From there, the question stops being about API syntax and becomes about which layer of your app is functional and which is content — and that distinction, more than anything else, decides whether the adoption ends up looking native.
Useful links:



