DEV Community

Nam Tran
Nam Tran

Posted on

Liquid Glass in Swift: Official Best Practices for iOS 26 & macOS Tahoe

iOS 26 and macOS Tahoe introduce Liquid Glass—Apple's unified design language. This guide compiles official Apple guidelines from WWDC 2025 sessions to help you integrate Liquid Glass correctly.

What is Liquid Glass?

From Apple's WWDC 2025 Session 219:

"Liquid Glass is a new digital meta-material that dynamically bends and shapes light."

Key difference: Liquid Glass uses "Lensing"—bending and concentrating light, not scattering it like traditional blur.

Mac OS 26 Widgets dimming style

iOS 26 Navigationbar Liquid Glass style


Part 1: What You Get FREE (Zero Code)

Recompile with Xcode 26 and these get Liquid Glass automatically:

iOS 26:

  • NavigationBar, TabBar, Toolbar
  • Sheets, Popovers, Menus, Alerts
  • Search bars, Control Center
  • Toggles, Sliders, Pickers (during interaction)

macOS Tahoe:

  • Toolbar, Sidebar, Menu bar, Dock
  • Window controls, NSPopover, Sheets
// This automatically gets Liquid Glass!
NavigationStack {
    List(items) { item in
        Text(item.name)
    }
    .toolbar {
        ToolbarItem(placement: .topBarTrailing) {
            Button("Add", systemImage: "plus") { }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Part 2: The Golden Rule - Navigation Layer Only

From Apple WWDC 2025:

"Liquid Glass is best reserved for the navigation layer that floats above the content of your app."

✅ Use Glass For:

  • Toolbars, TabBars, Sidebars
  • Floating action buttons
  • Sheets, popovers, menus

❌ Never Use Glass For:

  • Content layer (lists, cards, tables)
  • Full-screen backgrounds
  • Scrollable content
// ❌ WRONG - Glass on content
List {
    ForEach(items) { item in
        Text(item.name)
            .glassEffect()  // DON'T!
    }
}

// ✅ CORRECT - Glass only on floating controls
ZStack {
    List { /* content without glass */ }

    VStack {
        Spacer()
        FloatingButton()
            .glassEffect(.regular.interactive())
    }
}
Enter fullscreen mode Exit fullscreen mode

Part 3: Never Stack Glass on Glass

From Apple WWDC 2025:

"Always avoid glass on glass. Stacking Liquid Glass elements can quickly make the interface feel cluttered."

Why? Glass cannot properly sample other glass.

// ❌ WRONG
VStack {
    HeaderView().glassEffect()
    ContentView().glassEffect()  // Glass on glass!
}

// ✅ CORRECT - Single glass layer
ZStack {
    ContentView()  // No glass
    FloatingControls().glassEffect()  // Single layer
}
Enter fullscreen mode Exit fullscreen mode

Part 4: GlassEffectContainer - The Essential API

From Apple WWDC 2025 Session 323:

"Group multiple glass elements within a GlassEffectContainer because glass cannot sample other glass."

Always Use Container for Multiple Elements

// ✅ CORRECT
GlassEffectContainer {
    HStack(spacing: 16) {
        Button("Edit") { }.glassEffect()
        Button("Share") { }.glassEffect()
        Button("Delete") { }.glassEffect()
    }
}

// ❌ WRONG - Without container
HStack {
    Button("Edit") { }.glassEffect()
    Button("Share") { }.glassEffect()
}
// Each samples independently = inconsistent!
Enter fullscreen mode Exit fullscreen mode

Benefits of GlassEffectContainer

  1. Shared sampling region - Consistent appearance
  2. Better performance - Single render pass
  3. Enables morphing - Fluid transitions
  4. Automatic grouping - Elements blend when close

Spacing Parameter

GlassEffectContainer(spacing: 30) {
    // Elements within 30pt morph together
    HStack(spacing: 20) {
        Button("A") { }.glassEffect()
        Button("B") { }.glassEffect()
    }
}
Enter fullscreen mode Exit fullscreen mode

Part 5: Morphing with glassEffectID

Create fluid transitions between glass elements:

struct ExpandableMenu: View {
    @State private var isExpanded = false
    @Namespace private var namespace

    var body: some View {
        GlassEffectContainer(spacing: 20) {
            HStack(spacing: 16) {
                if isExpanded {
                    Button("Camera", systemImage: "camera") { }
                        .glassEffect(.regular.interactive())
                        .glassEffectID("camera", in: namespace)

                    Button("Photos", systemImage: "photo") { }
                        .glassEffect(.regular.interactive())
                        .glassEffectID("photos", in: namespace)
                }

                Button {
                    withAnimation(.bouncy) {
                        isExpanded.toggle()
                    }
                } label: {
                    Image(systemName: isExpanded ? "xmark" : "plus")
                        .frame(width: 44, height: 44)
                }
                .buttonStyle(.glassProminent)
                .buttonBorderShape(.circle)
                .glassEffectID("toggle", in: namespace)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Requirements for morphing:

  1. Elements in same GlassEffectContainer
  2. Each has glassEffectID with shared namespace
  3. Animation applied to state changes

Part 6: Regular vs Clear Variants

From Apple WWDC 2025:

"They should never be mixed, as they each have their own characteristics."

Regular (Default) - Use for Most Cases

.glassEffect(.regular)  // or just .glassEffect()
Enter fullscreen mode Exit fullscreen mode

Clear - Only When ALL Three Conditions Met:

  1. Over media-rich content (photos, videos)
  2. Content won't suffer from dimming layer
  3. Content above is bold and bright
// Photo editor controls
ZStack {
    Image("photo")

    HStack {
        Button("Filters") { }
            .glassEffect(.clear.interactive())
    }
}
Enter fullscreen mode Exit fullscreen mode

Never Mix Variants

// ❌ WRONG
HStack {
    Button("A") { }.glassEffect(.regular)
    Button("B") { }.glassEffect(.clear)  // Don't mix!
}
Enter fullscreen mode Exit fullscreen mode

Part 7: Tinting - Primary Actions Only

From Apple WWDC 2025:

"Tinting should only be used to bring emphasis to primary elements and actions."

// ✅ CORRECT - Only primary action tinted
HStack {
    Button("Cancel") { }
        .buttonStyle(.glass)  // No tint

    Button("Save") { }
        .buttonStyle(.glassProminent)
        .tint(.blue)  // Primary action
}

// ❌ WRONG - Everything tinted
HStack {
    Button("A") { }.glassEffect(.regular.tint(.blue))
    Button("B") { }.glassEffect(.regular.tint(.green))
    Button("C") { }.glassEffect(.regular.tint(.red))
}
// When everything is tinted, nothing stands out!
Enter fullscreen mode Exit fullscreen mode

Part 8: Button Styles

Style Appearance Use Case
.glass Translucent Secondary actions
.glassProminent Opaque Primary actions
HStack {
    Button("Cancel") { }
        .buttonStyle(.glass)

    Button("Confirm") { }
        .buttonStyle(.glassProminent)
        .tint(.green)
}
Enter fullscreen mode Exit fullscreen mode

Part 9: Interactive Modifier (iOS Only)

// iOS - Touch feedback
Button("Tap") { }
    .glassEffect(.regular.interactive())
Enter fullscreen mode Exit fullscreen mode

Behaviors: Scales, bounces, shimmers, illuminates from touch point.


Part 10: macOS Specifics

AppKit Glass Bezel

let button = NSButton()
button.bezelStyle = .glass
button.bezelColor = .systemBlue
Enter fullscreen mode Exit fullscreen mode

Automatic Toolbar Grouping

AppKit groups multiple toolbar buttons on one glass piece. Use NSToolbarItemGroup or spacers to override.

Sidebar Ambient Reflection

On macOS/iPad, sidebars reflect light from nearby colorful content automatically.


Part 11: Accessibility - Automatic

From Apple WWDC 2025:

"These are available automatically whenever you use the new material."

Setting Effect
Reduce Transparency Frostier glass
Increase Contrast Black/white with border
Reduce Motion Disabled elastic effects

No extra code needed!


Quick Reference: Do's and Don'ts

✅ DO

// Container for multiple elements
GlassEffectContainer {
    HStack {
        Button("A") { }.glassEffect()
        Button("B") { }.glassEffect()
    }
}

// Morphing IDs
.glassEffectID("btn", in: namespace)

// Interactive on iOS
.glassEffect(.regular.interactive())

// Tint only primary actions
.buttonStyle(.glassProminent).tint(.blue)
Enter fullscreen mode Exit fullscreen mode

❌ DON'T

// Glass on content
List { ... }.glassEffect()

// Glass on glass
VStack {
    View1().glassEffect()
    View2().glassEffect()
}

// Mix variants
.glassEffect(.regular)
.glassEffect(.clear)

// Tint everything
.glassEffect(.regular.tint(.blue))

// Multiple glass without container
HStack {
    Button("A") { }.glassEffect()
    Button("B") { }.glassEffect()
}
Enter fullscreen mode Exit fullscreen mode

API Quick Reference

// Basic
.glassEffect()
.glassEffect(.regular)
.glassEffect(.clear)

// With shape
.glassEffect(.regular, in: .capsule)
.glassEffect(.regular, in: .circle)

// Modifiers
.glassEffect(.regular.tint(.blue))
.glassEffect(.regular.interactive())

// Container
GlassEffectContainer(spacing: 30) { }

// Morphing
.glassEffectID("id", in: namespace)

// Button styles
.buttonStyle(.glass)
.buttonStyle(.glassProminent)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Remember the core principles:

  1. Navigation layer only - Never content
  2. Never stack glass on glass
  3. Use GlassEffectContainer for multiple elements
  4. Tint selectively - Primary actions only
  5. Trust the system - Accessibility is automatic

Official Resources:

Apple's Official Documentation

Top comments (0)