At WWDC 2025, Apple unveiled Liquid Glass - the most significant design evolution since iOS 7. This new visual language brings a translucent, dynamic material to controls, navigation, icons, and more. Here's everything you need to know about implementing Liquid Glass in your iOS 26 SwiftUI apps.

What is Liquid Glass?

Liquid Glass is Apple's new design material that reflects and refracts its surroundings while dynamically transforming to bring focus to content. It creates a new level of vitality across the entire interface.

Key characteristics:

  • Translucent - Content behind glass elements shows through with a blur effect
  • Dynamic - The material responds to what's beneath it in real-time
  • Morphing - Glass elements can smoothly transition between states
  • Ambient - Subtle reflections and refractions create depth

Liquid Glass extends across iOS 26, iPadOS 26, macOS Tahoe, watchOS 26, and tvOS 26 - creating visual harmony while respecting each platform's unique qualities.

Automatic Adoption

Great news: if your app already uses native SwiftUI controls, you get Liquid Glass automatically. Simply recompile against the iOS 26 SDK and these components adopt the new design:

  • Tab bars
  • Toolbars
  • Navigation bars
  • Sheets
  • Alerts
  • System buttons

Toolbar items now float on a Liquid Glass surface that automatically adapts to content beneath. Partial-height sheets get an inset glass background that morphs as the sheet expands.

Core SwiftUI APIs

iOS 26 introduces three key APIs for working with Liquid Glass:

1. glassEffect() Modifier

Apply the glass material to any view:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, Glass!")
                .padding()
        }
        .glassEffect()
    }
}

The glassEffect() modifier transforms your view with the translucent glass material.

2. GlassEffectContainer

Coordinate how multiple glass elements blend and transition:

struct CardView: View {
    var body: some View {
        GlassEffectContainer {
            VStack(spacing: 16) {
                HeaderView()
                    .glassEffect()

                ContentView()
                    .glassEffect()

                FooterView()
                    .glassEffect()
            }
        }
    }
}

Elements within a GlassEffectContainer share a visual style and can participate in coordinated animations.

3. glassEffectID() for Morphing

Create smooth morphing transitions between glass elements across states:

struct MorphingCard: View {
    @Namespace private var animation
    @State private var isExpanded = false

    var body: some View {
        GlassEffectContainer {
            if isExpanded {
                ExpandedCard()
                    .glassEffect()
                    .glassEffectID("card", in: animation)
            } else {
                CollapsedCard()
                    .glassEffect()
                    .glassEffectID("card", in: animation)
            }
        }
        .onTapGesture {
            withAnimation(.spring()) {
                isExpanded.toggle()
            }
        }
    }
}

The glassEffectID associates related glass elements, enabling the system to morph between them seamlessly.

Building a Glass Card Component

Here's a complete example of a reusable glass card:

struct GlassCard<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        content
            .padding(20)
            .glassEffect()
            .clipShape(RoundedRectangle(cornerRadius: 20))
    }
}

// Usage
struct ProfileView: View {
    var body: some View {
        ZStack {
            // Background image for the glass to reflect
            Image("background")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .ignoresSafeArea()

            GlassCard {
                VStack(alignment: .leading, spacing: 12) {
                    HStack {
                        Image(systemName: "person.circle.fill")
                            .font(.largeTitle)

                        VStack(alignment: .leading) {
                            Text("John Appleseed")
                                .font(.headline)
                            Text("@john.bsky.social")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                    }

                    Divider()

                    Text("Building the future of social media")
                        .font(.body)
                }
            }
            .padding()
        }
    }
}

Glass Toolbars and Navigation

Toolbars in iOS 26 automatically use Liquid Glass. Items are grouped on a floating glass surface:

struct FeedView: View {
    var body: some View {
        NavigationStack {
            ScrollView {
                // Your content
            }
            .navigationTitle("Feed")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: compose) {
                        Image(systemName: "square.and.pencil")
                    }
                }

                ToolbarItem(placement: .secondaryAction) {
                    Button(action: refresh) {
                        Image(systemName: "arrow.clockwise")
                    }
                }
            }
        }
    }
}

The toolbar items appear on a glass bar that floats above your content and responds to scrolling.

Glass Sheets

Sheets now feature glass backgrounds that adapt to their presentation height:

struct MainView: View {
    @State private var showSettings = false

    var body: some View {
        ContentView()
            .sheet(isPresented: $showSettings) {
                SettingsView()
                    .presentationDetents([.medium, .large])
                    .presentationBackground(.glass)
            }
    }
}

At smaller heights, the sheet edges curve inward to nest within the display corners. When expanded to full height, the glass gradually becomes opaque and anchors to the screen edges.

Custom Glass Buttons

Create interactive glass buttons with hover and press states:

struct GlassButton: View {
    let title: String
    let icon: String
    let action: () -> Void

    @State private var isPressed = false

    var body: some View {
        Button(action: action) {
            HStack(spacing: 8) {
                Image(systemName: icon)
                Text(title)
            }
            .padding(.horizontal, 20)
            .padding(.vertical, 12)
            .glassEffect()
            .clipShape(Capsule())
            .scaleEffect(isPressed ? 0.95 : 1.0)
        }
        .buttonStyle(.plain)
        .onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in
            withAnimation(.easeInOut(duration: 0.1)) {
                isPressed = pressing
            }
        }, perform: {})
    }
}

// Usage
GlassButton(title: "Follow", icon: "plus") {
    // Follow action
}

Animated Glass Transitions

Create dynamic transitions between glass states:

struct ExpandableCard: View {
    @Namespace private var glassNamespace
    @State private var isExpanded = false

    var body: some View {
        GlassEffectContainer {
            VStack {
                if isExpanded {
                    // Expanded state
                    VStack(spacing: 20) {
                        Image(systemName: "star.fill")
                            .font(.system(size: 60))

                        Text("Featured Content")
                            .font(.title)

                        Text("This card has expanded to show more details about the featured content.")
                            .multilineTextAlignment(.center)

                        Button("Collapse") {
                            withAnimation(.spring(response: 0.4)) {
                                isExpanded = false
                            }
                        }
                    }
                    .padding(40)
                    .glassEffect()
                    .glassEffectID("card", in: glassNamespace)
                } else {
                    // Collapsed state
                    HStack {
                        Image(systemName: "star.fill")
                        Text("Featured")
                        Spacer()
                        Image(systemName: "chevron.right")
                    }
                    .padding(20)
                    .glassEffect()
                    .glassEffectID("card", in: glassNamespace)
                    .onTapGesture {
                        withAnimation(.spring(response: 0.4)) {
                            isExpanded = true
                        }
                    }
                }
            }
        }
    }
}

UIKit and SwiftUI Hybrid Implementation

Many production apps use both UIKit and SwiftUI. Here's how to integrate Liquid Glass in hybrid architectures:

Hosting SwiftUI Glass Views in UIKit

import UIKit
import SwiftUI

class GlassHostingController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let glassView = GlassOverlayView()
        let hostingController = UIHostingController(rootView: glassView)

        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            hostingController.view.heightAnchor.constraint(equalToConstant: 80)
        ])

        hostingController.didMove(toParent: self)
    }
}

struct GlassOverlayView: View {
    var body: some View {
        HStack(spacing: 20) {
            ForEach(["house", "magnifyingglass", "bell", "person"], id: \.self) { icon in
                Image(systemName: icon)
                    .font(.title2)
            }
        }
        .frame(maxWidth: .infinity)
        .padding()
        .glassEffect()
    }
}

Using UIKit Views Behind Glass

struct MapWithGlassOverlay: View {
    var body: some View {
        ZStack {
            // UIKit MapView wrapped for SwiftUI
            MapViewRepresentable()
                .ignoresSafeArea()

            VStack {
                Spacer()

                // Glass controls floating over the map
                HStack(spacing: 16) {
                    GlassButton(icon: "location.fill", action: centerOnUser)
                    GlassButton(icon: "map.fill", action: toggleMapStyle)
                    GlassButton(icon: "square.stack.3d.up", action: show3D)
                }
                .padding()
                .glassEffect()
                .clipShape(Capsule())
                .padding(.bottom, 40)
            }
        }
    }
}

Common Pitfalls and Debugging

Avoid these common mistakes when implementing Liquid Glass:

1. Stacking Glass on Glass

Glass cannot sample other glass elements, leading to visual artifacts:

// DON'T do this - glass stacking causes issues
VStack {
    Text("Outer")
        .glassEffect()  // Parent glass

    Text("Inner")
        .glassEffect()  // Nested glass - problematic!
}

// DO this instead - use GlassEffectContainer
GlassEffectContainer {
    VStack {
        Text("Outer")
            .glassEffect()

        Text("Inner")
            // No glass here, or use different visual treatment
            .background(.thinMaterial)
    }
}

2. Forgetting GlassEffectContainer for Coordination

// Without container - glass effects don't coordinate
VStack {
    Card1().glassEffect()
    Card2().glassEffect()  // May have inconsistent appearance
}

// With container - consistent glass styling
GlassEffectContainer {
    VStack {
        Card1().glassEffect()
        Card2().glassEffect()  // Visually coordinated
    }
}

3. Missing glassEffectID Namespace

// This won't morph properly
struct BrokenMorphing: View {
    @State private var isExpanded = false

    var body: some View {
        if isExpanded {
            ExpandedView()
                .glassEffect()
                .glassEffectID("card", in: ???)  // Missing namespace!
        }
    }
}

// Correct implementation
struct WorkingMorphing: View {
    @Namespace private var glassNamespace  // Define namespace
    @State private var isExpanded = false

    var body: some View {
        GlassEffectContainer {
            if isExpanded {
                ExpandedView()
                    .glassEffect()
                    .glassEffectID("card", in: glassNamespace)
            } else {
                CollapsedView()
                    .glassEffect()
                    .glassEffectID("card", in: glassNamespace)
            }
        }
    }
}

4. Glass Over Solid Backgrounds

// Glass looks flat over solid colors
ZStack {
    Color.gray  // Solid background - glass loses depth

    Text("Hello")
        .glassEffect()
}

// Glass shines over varied content
ZStack {
    Image("photo")  // Dynamic background content
        .resizable()

    Text("Hello")
        .glassEffect()  // Glass reflects and refracts beautifully
}

Debugging Tips

  • Use Debug Hierarchy: Xcode's View Debugger shows glass layers and their relationships
  • Check for overlapping glass: Multiple glass views at the same z-position cause artifacts
  • Profile GPU usage: Instruments shows if glass effects are causing performance issues
  • Test with Reduce Transparency: Ensure your UI is still usable when glass is disabled

Advanced Glass Techniques

Glass with Dynamic Content

struct LiveGlassCard: View {
    @State private var posts: [Post] = []

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(posts) { post in
                    PostRow(post: post)
                        .padding()
                        .glassEffect()
                        .clipShape(RoundedRectangle(cornerRadius: 16))
                }
            }
            .padding()
        }
        .background {
            // Animated gradient behind glass
            AnimatedGradient()
                .ignoresSafeArea()
        }
    }
}

Glass Tab Bar with Badge

struct GlassTabBar: View {
    @Binding var selectedTab: Int
    let tabs: [(icon: String, badge: Int?)]

    var body: some View {
        HStack(spacing: 0) {
            ForEach(tabs.indices, id: \.self) { index in
                Button {
                    withAnimation(.spring(response: 0.3)) {
                        selectedTab = index
                    }
                } label: {
                    ZStack(alignment: .topTrailing) {
                        Image(systemName: tabs[index].icon)
                            .font(.title2)
                            .foregroundStyle(selectedTab == index ? .primary : .secondary)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 12)

                        if let badge = tabs[index].badge, badge > 0 {
                            Text("\(badge)")
                                .font(.caption2.bold())
                                .foregroundStyle(.white)
                                .padding(4)
                                .background(.red)
                                .clipShape(Circle())
                                .offset(x: -8, y: 4)
                        }
                    }
                }
                .buttonStyle(.plain)
            }
        }
        .padding(.horizontal)
        .glassEffect()
        .clipShape(Capsule())
    }
}

Conditional Glass Based on Scroll Position

struct ScrollAwareGlassHeader: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        ZStack(alignment: .top) {
            ScrollView {
                GeometryReader { geo in
                    Color.clear.preference(
                        key: ScrollOffsetKey.self,
                        value: geo.frame(in: .named("scroll")).minY
                    )
                }
                .frame(height: 0)

                // Your content here
                ContentView()
                    .padding(.top, 60)
            }
            .coordinateSpace(name: "scroll")
            .onPreferenceChange(ScrollOffsetKey.self) { offset in
                scrollOffset = offset
            }

            // Header with conditional glass
            HeaderView()
                .padding()
                .background {
                    if scrollOffset < -20 {
                        Rectangle()
                            .glassEffect()
                            .transition(.opacity)
                    }
                }
                .animation(.easeInOut(duration: 0.2), value: scrollOffset < -20)
        }
    }
}

Accessibility Considerations

Liquid Glass includes important accessibility features:

  • Reduce Transparency - Users can reduce glass transparency in Settings for improved legibility
  • Tinted vs Clear - Settings option to use tinted glass instead of clear
  • Increase Contrast - Glass effects adapt when this accessibility setting is enabled
  • Dynamic Type - Glass containers scale appropriately with text size changes

Always test your glass implementations with accessibility settings enabled:

// Preview with reduce transparency
#Preview("Reduce Transparency") {
    ContentView()
        .environment(\.accessibilityReduceTransparency, true)
}

Best Practices

Do:

  • Use glass for floating UI - Toolbars, action buttons, overlays
  • Provide contrasting backgrounds - Glass looks best over varied, colorful content
  • Group related elements - Use GlassEffectContainer for coordinated effects
  • Test legibility - Ensure text remains readable over all backgrounds
  • Embrace the morphing - Use glassEffectID for smooth state transitions

Don't:

  • Overuse glass - Not every element needs to be translucent
  • Stack glass on glass - Multiple layers become muddy and hard to read
  • Use glass for dense text - Long-form content should have solid backgrounds
  • Ignore accessibility - Test with Reduce Transparency enabled

Migration from iOS 18

If your app already uses SwiftUI materials, here's how to migrate to Liquid Glass:

API Mapping Table

iOS 18 API iOS 26 Equivalent Notes
.background(.ultraThinMaterial) .glassEffect() Glass adapts automatically
.background(.thinMaterial) .glassEffect() No thickness levels needed
.background(.regularMaterial) .glassEffect() System handles opacity
.background(.thickMaterial) .glassEffect() Or use solid background
.matchedGeometryEffect() .glassEffectID() For glass-specific morphing
Custom blur effects GlassEffectContainer Coordinated glass rendering

Step-by-Step Migration

Step 1: Update your deployment target to iOS 26 in Xcode.

Step 2: Search your codebase for material usage:

// Find and replace patterns:
// OLD: .background(.ultraThinMaterial)
// NEW: .glassEffect()

// OLD: .background(.regularMaterial)
// NEW: .glassEffect()

// For matched geometry with glass:
// OLD: .matchedGeometryEffect(id: "card", in: namespace)
// NEW: .glassEffectID("card", in: namespace)

Step 3: Wrap related glass elements in containers:

// Before (iOS 18)
VStack {
    Header().background(.thinMaterial)
    Content().background(.thinMaterial)
}

// After (iOS 26)
GlassEffectContainer {
    VStack {
        Header().glassEffect()
        Content().glassEffect()
    }
}

Step 4: Test with accessibility settings enabled (Reduce Transparency, Increase Contrast).

Step 5: Profile performance and optimize if needed.

Backward Compatibility

If you need to support iOS 18 and iOS 26:

extension View {
    @ViewBuilder
    func adaptiveGlass() -> some View {
        if #available(iOS 26, *) {
            self.glassEffect()
        } else {
            self.background(.ultraThinMaterial)
        }
    }
}

// Usage
Text("Hello")
    .padding()
    .adaptiveGlass()
    .clipShape(RoundedRectangle(cornerRadius: 12))

Performance Tips

Liquid Glass is GPU-accelerated but can impact performance:

  • Limit animated glass - Don't animate too many glass elements simultaneously
  • Use lazy loading - LazyVStack/LazyHStack for lists with glass cells
  • Profile regularly - Use Instruments to monitor GPU usage
  • Consider reduced motion - Simplify animations when this setting is enabled

Frequently Asked Questions

What is Apple Liquid Glass?

Liquid Glass is Apple's new design language in iOS 26, featuring translucent materials that reflect and refract surroundings for a more dynamic, immersive UI.

How do I add Liquid Glass to my SwiftUI app?

Use .glassEffect() on views, wrap related elements in GlassEffectContainer, and use .glassEffectID() for morphing transitions.

Will my app automatically get Liquid Glass?

Yes, for native controls like toolbars, sheets, and tab bars when you recompile against iOS 26 SDK. Custom views need manual implementation.

Does Liquid Glass work on all Apple platforms?

Yes - iOS 26, iPadOS 26, macOS Tahoe, watchOS 26, and tvOS 26 all support Liquid Glass with platform-appropriate adaptations.

Resources

Related Reading

Explore more iOS development guides from the Skyscraper blog:


About Skyscraper

Skyscraper is a Bluesky client built entirely with SwiftUI, updated for iOS 26 with full Liquid Glass support. The app showcases modern iOS development practices including the ATProtocol integration, custom feeds, and native iOS design patterns. Our development team shares insights from building a production SwiftUI app to help other iOS developers.