SwiftUI Liquid Glass: Build iOS 26 Translucent UI — ContentBuffer guide

SwiftUI Liquid Glass: Build iOS 26 Translucent UI

K
Kodetra Technologies··9 min read Intermediate

Summary

Master glassEffect, GlassEffectContainer and morphing in iOS 26 SwiftUI.

Why Liquid Glass changed the iOS 26 UI playbook

Liquid Glass is Apple's new material in iOS 26, iPadOS 26, macOS Tahoe and visionOS 26. It refracts the content underneath, reflects ambient light along its edge and morphs smoothly between shapes. Apple rewrote system UI on top of it: tab bars, sheets, the Control Center, the Dynamic Island. If your app still uses opaque cards or .ultraThinMaterial blurs, it now looks dated next to first-party apps.

The good news: SwiftUI exposes Liquid Glass through a small, ergonomic API. The whole surface fits on one mental page — .glassEffect(), GlassEffectContainer, glassEffectID, GlassEffectTransition. By the end of this guide you will have built a floating action toolbar that uses every one of them, plus you will know the gotchas that bite teams the first week.

Prerequisites

  • Xcode 26 (or newer) with the iOS 26 SDK installed.
  • An iOS 26 simulator or device. Liquid Glass falls back to a plain material on older OSes — wrap fallback code in #available(iOS 26, *) if you ship to iOS 17/18.
  • Working knowledge of SwiftUI: View, modifiers, @State, @Namespace.
  • A target with SwiftUI.Liquid_Glass available (it ships in the base SwiftUI module on iOS 26 — no extra import needed).

Step 1 — Apply your first glass effect

Start with a plain text view. The .glassEffect() modifier wraps it in a translucent capsule with the system blur and rim light:

import SwiftUI

struct GlassHello: View {
    var body: some View {
        ZStack {
            // The wallpaper-like background is what makes Liquid Glass shine.
            Image("hero")
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()

            Text("Hello, Liquid Glass")
                .font(.title2.weight(.semibold))
                .padding(.horizontal, 20)
                .padding(.vertical, 12)
                .glassEffect() // default: .regular variant, .capsule shape
        }
    }
}

Run it and you should see the text floating on a frosted capsule that picks up colour from the photo behind it.

Variants and shapes

The full signature is glassEffect(_ glass: Glass = .regular, in shape: Shape = Capsule(), isEnabled: Bool = true). The Glass type has three built-in variants:

VariantWhen to use
.regularDefault. Best contrast, works on any background.
.clearMore transparent. Use over photos or video for an airy feel.
.identityNo glass — useful as an if branch placeholder.

Switch the shape with any Shape conforming type. The two patterns you will use 90% of the time:

// Pill button
Button("Save") { save() }
    .padding(.horizontal, 18).padding(.vertical, 10)
    .glassEffect(.regular.tint(.accentColor), in: .capsule)

// Card
VStack(alignment: .leading) {
    Text("Daily streak").font(.caption)
    Text("12 days").font(.title.bold())
}
.padding(20)
.glassEffect(.clear, in: RoundedRectangle(cornerRadius: 24))

Two modifiers on Glass are worth memorising. .tint(_:) dyes the material — keep it under 50% saturation or you will lose the refraction. .interactive() lights up the glass on touch with the same scale-and-shimmer Apple uses on system controls.

Text("Tap me")
    .padding()
    .glassEffect(.regular.tint(.blue).interactive())

Step 2 — Group glass with GlassEffectContainer

Apply .glassEffect() to two views sitting next to each other and you get two separate glass shapes. That looks fine when they are far apart, but if they overlap or sit within a few points of each other you will see a hard seam and the lighting will fight. GlassEffectContainer is the fix. It merges overlapping shapes into one continuous piece of glass and ensures the rim light and refraction stay consistent.

struct Toolbar: View {
    var body: some View {
        GlassEffectContainer(spacing: 12) {
            HStack(spacing: 12) {
                ToolbarIcon(symbol: "house.fill")
                ToolbarIcon(symbol: "magnifyingglass")
                ToolbarIcon(symbol: "bell.fill")
                ToolbarIcon(symbol: "person.crop.circle")
            }
            .padding(8)
        }
    }
}

private struct ToolbarIcon: View {
    let symbol: String
    var body: some View {
        Image(systemName: symbol)
            .font(.title3)
            .frame(width: 44, height: 44)
            .glassEffect(in: .circle)
    }
}

The spacing: argument is the morph threshold. When two glass shapes inside the container come within spacing points of each other, SwiftUI fuses them. Set it to 0 for a strict row of separate circles; bump it to 20 for an Apple-Music-style pill that grows around its buttons.

Sample I/O

If you log frame counts with Self._printChanges() in the icon view, you will see that wrapping four icons in a container instead of applying glass per-icon drops draw calls roughly in half — the GPU rasterises one merged shape instead of four. That is the rendering-performance win Apple advertises.

Step 3 — Morph between glass shapes with glassEffectID

The killer demo for Liquid Glass is shape morphing — tapping an icon and watching it stretch into a sheet, the way Apple's Camera app does. SwiftUI does the heavy lifting through glassEffectID(_:in:). Mark two glass views with the same ID inside the same container and inside a matched-namespace and SwiftUI will tween between them.

struct ExpandingComposer: View {
    @State private var expanded = false
    @Namespace private var glassNS

    var body: some View {
        GlassEffectContainer(spacing: 20) {
            if expanded {
                VStack(alignment: .leading, spacing: 12) {
                    TextField("What's on your mind?", text: .constant(""))
                        .textFieldStyle(.plain)
                    HStack {
                        Spacer()
                        Button("Post") { expanded = false }
                            .buttonStyle(.borderedProminent)
                    }
                }
                .padding(20)
                .frame(maxWidth: .infinity)
                .glassEffect(in: RoundedRectangle(cornerRadius: 28))
                .glassEffectID("composer", in: glassNS)
            } else {
                Button {
                    withAnimation(.smooth(duration: 0.45)) { expanded = true }
                } label: {
                    Image(systemName: "square.and.pencil")
                        .font(.title2)
                        .frame(width: 56, height: 56)
                }
                .glassEffect(.regular.tint(.accentColor), in: .circle)
                .glassEffectID("composer", in: glassNS)
            }
        }
        .padding()
    }
}

Three things to notice. First, the if/else branches share the same glassEffectID string — that is what tells SwiftUI "these are the same logical surface, animate them." Second, the GlassEffectContainer is essential; without it, the modifier silently no-ops. Third, you still need an explicit withAnimation at the call site. The morphing is driven by SwiftUI's standard transaction system, not magic.

Step 4 — Customise enter and exit with GlassEffectTransition

By default, a glass view added to the hierarchy slides in via matched geometry from its sibling, and disappears the same way. Override that with .glassEffectTransition(_:):

TransitionEffect
.identityPop in/out instantly. Use for non-essential overlays.
.matchedGeometryDefault. Slide along the matched namespace.
.materializeFade and scale like a system material. Best for toasts and tooltips.
if showToast {
    Label("Saved", systemImage: "checkmark.circle.fill")
        .padding(.horizontal, 16).padding(.vertical, 10)
        .glassEffect(.regular.tint(.green), in: .capsule)
        .glassEffectTransition(.materialize)
}

Step 5 — Wire it into a real screen

Here is the full floating toolbar that combines steps 2–4. Drop it into any NavigationStack and it sits above your content the way iOS 26's system tab bar does.

struct FeedView: View {
    @State private var composing = false
    @Namespace private var glassNS

    var body: some View {
        ZStack(alignment: .bottom) {
            ScrollView { feed }

            GlassEffectContainer(spacing: 20) {
                if composing {
                    composer
                        .glassEffectID("primary", in: glassNS)
                } else {
                    HStack(spacing: 12) {
                        IconButton("house.fill") {}
                        IconButton("magnifyingglass") {}
                        IconButton("bell.fill") {}
                        IconButton("square.and.pencil") {
                            withAnimation(.smooth) { composing = true }
                        }
                    }
                    .padding(8)
                    .glassEffectID("primary", in: glassNS)
                }
            }
            .padding(.horizontal, 16)
            .padding(.bottom, 12)
        }
    }

    private var feed: some View { /* your feed list */ EmptyView() }

    private var composer: some View {
        VStack(spacing: 12) {
            TextField("Share an update", text: .constant(""))
            HStack {
                Spacer()
                Button("Cancel") { withAnimation(.smooth) { composing = false } }
                Button("Post")   { withAnimation(.smooth) { composing = false } }
                    .buttonStyle(.borderedProminent)
            }
        }
        .padding(20)
        .frame(maxWidth: .infinity)
        .glassEffect(in: RoundedRectangle(cornerRadius: 28))
    }
}

private struct IconButton: View {
    let symbol: String
    let action: () -> Void
    init(_ symbol: String, action: @escaping () -> Void = {}) {
        self.symbol = symbol; self.action = action
    }
    var body: some View {
        Button(action: action) {
            Image(systemName: symbol)
                .font(.title3)
                .frame(width: 44, height: 44)
        }
        .glassEffect(in: .circle)
    }
}

Common pitfalls

Stacking glass on glass

Putting .glassEffect() on a view that is already sitting on a glass surface (a sheet, a popover, a navigation bar) doubles the blur and produces muddy results. Apple's HIG calls this out explicitly: do not nest glass. If you need an emphasised control on a glass surface, use a solid .borderedProminent button or a .tinted background instead.

Forgetting the container

glassEffectID only animates when both source and destination live inside the same GlassEffectContainer. If your morph is not firing, that is the first thing to check. The second is whether you wrapped your state change in withAnimation — without it the views snap.

Tints that kill the effect

Heavily saturated tints flatten the refraction. Aim for the system accent colour at default opacity, or a custom colour with alpha ≤ 0.6. If you need a strong brand colour, layer it under the glass as a coloured background rectangle rather than tinting the glass itself.

Backgrounds that show nothing

Liquid Glass looks like a plain blur on a flat background because there is nothing to refract. Test against photo content, gradients, or a moving SwiftUI canvas. In previews, set a colourful Image behind your glass view or you will keep tweaking values that look fine on a dark background and ugly in the wild.

Falling back to iOS 17/18

If you ship to older OSes, gate the modifier behind availability and provide a sensible fallback:

extension View {
    @ViewBuilder
    func appGlass<S: Shape>(in shape: S) -> some View {
        if #available(iOS 26, *) {
            self.glassEffect(in: shape)
        } else {
            self.background(.ultraThinMaterial, in: shape)
        }
    }
}

Accessibility

Liquid Glass respects Reduce Transparency automatically — the material becomes opaque. Do not override that. Also test with Increase Contrast turned on; high-contrast users see a thicker rim and reduced refraction, which can change how your tints read.

When (and when not) to use Liquid Glass

Liquid Glass is a foreground material. The strongest use cases share three traits: the surface floats above other content, the user can drag or scroll content underneath it, and the surface holds controls or short text. Tab bars, floating action buttons, toasts, sheets, badges, status pills — all natural fits. Static content panels and reading surfaces are not. A glass paragraph card in the middle of a feed reads worse than a plain card because the user has nothing to refract against once they scroll past the first photo.

The rule of thumb that has held up in production reviews: if removing the glass effect would make the surface harder to distinguish from its content, keep the glass; if it would make the surface easier to read, remove the glass. The material is for separation, not decoration.

Performance characteristics

Each glass shape is a GPU pass: capture the content behind, blur it, apply the rim light, composite. On A17 Pro and newer, that is essentially free for a handful of shapes. The cost climbs quickly when you scatter glass across a long list. Three practical rules:

  1. Cluster glass shapes inside a single GlassEffectContainer. The container merges overlapping shapes into one pass.
  2. Avoid .glassEffect() inside List or LazyVStack row content. Apply it to the chrome (header, floating buttons) instead of every cell.
  3. Disable the effect for users on Reduce Motion + Reduce Transparency. You don't have to do anything special — SwiftUI honours both — but make sure your fallback layout is still legible when the material drops away.

Theming and brand colour

Brand teams ask the same question on day one: "can we make the glass match our colour?" The honest answer is a half-yes. Glass.tint(_:) dyes the material but you lose refraction at high opacities. The cleaner pattern is to place a brand-coloured shape under the glass:

VStack { /* content */ }
    .padding(20)
    .background(
        LinearGradient(colors: [.purple.opacity(0.7), .blue.opacity(0.6)],
                       startPoint: .topLeading, endPoint: .bottomTrailing),
        in: RoundedRectangle(cornerRadius: 28)
    )
    .glassEffect(.clear, in: RoundedRectangle(cornerRadius: 28))

The gradient sits behind the glass and the .clear variant lets it bleed through while still picking up the rim light. The result reads as branded glass instead of tinted plastic.

Quick reference

APIPurpose
.glassEffect(_:in:)Apply Liquid Glass to a view with a chosen variant + shape.
Glass.regular / .clear / .identityBuilt-in material variants.
Glass.tint(_:)Dye the glass. Keep alpha low.
Glass.interactive()Light up on touch with system feel.
GlassEffectContainerGroup glass views, merge overlapping shapes, enable morphing.
.glassEffectID(_:in:)Tag related glass views so they animate between states.
.glassEffectTransition(_:)Choose .matchedGeometry, .materialize, or .identity.

Next steps

You now have everything you need to ship Liquid Glass in production. A few directions worth exploring next:

  • Pair glassEffectID with the new NavigationTransition API for full-screen morphs.
  • Build a custom ToolbarStyle using GlassEffectContainer — system toolbars already do this, but you can theme yours.
  • Profile on an older device. Liquid Glass is GPU-accelerated, but stacking dozens of small glass shapes in a list still costs frames. Reach for one container per cluster, not one per element.
  • Read Apple's Applying Liquid Glass to custom views doc for the macOS and visionOS nuances when you ship cross-platform.

Liquid Glass is one of those rare API additions that looks like a cosmetic tweak and turns out to be a system rewrite. The surface area is small, but used well it lifts an app's perceived quality more than any other single iOS 26 change. Ship it deliberately, not everywhere.

Comments

Subscribe to join the conversation...

Be the first to comment