.agents/skills/swiftui-expert-skill/references/liquid-glass.md
Liquid Glass is Apple's new design language introduced in iOS 26. It provides translucent, dynamic surfaces that respond to content and user interaction. This reference covers the native SwiftUI APIs for implementing Liquid Glass effects.
All Liquid Glass APIs require iOS 26 or later. Always provide fallbacks:
if #available(iOS 26, *) {
// Liquid Glass implementation
} else {
// Fallback using materials
}
The primary modifier for applying glass effects to views:
.glassEffect(_ style: GlassEffectStyle = .regular, in shape: some Shape = .rect)
Text("Hello")
.padding()
.glassEffect() // Default regular style, rect shape
Text("Rounded Glass")
.padding()
.glassEffect(in: .rect(cornerRadius: 16))
Image(systemName: "star")
.padding()
.glassEffect(in: .circle)
Text("Capsule")
.padding(.horizontal, 20)
.padding(.vertical, 10)
.glassEffect(in: .capsule)
.glassEffect(.regular) // Standard glass appearance
.glassEffect(.prominent) // More visible, higher contrast
Add color tint to the glass:
.glassEffect(.regular.tint(.blue))
.glassEffect(.prominent.tint(.red.opacity(0.3)))
Make glass respond to touch/pointer hover:
// Interactive glass - responds to user interaction
.glassEffect(.regular.interactive())
// Combined with tint
.glassEffect(.regular.tint(.blue).interactive())
Important: Only use .interactive() on elements that actually respond to user input (buttons, tappable views, focusable elements).
Wraps multiple glass elements for proper visual grouping and spacing.
Glass cannot sample other glass. The glass material reflects and refracts light by sampling content from an area larger than itself. Nearby glass elements in different containers will produce inconsistent visual results because they cannot sample each other. GlassEffectContainer gives grouped elements a shared sampling region, ensuring a consistent appearance.
GlassEffectContainer {
HStack {
Button("One") { }
.glassEffect()
Button("Two") { }
.glassEffect()
}
}
Control the visual spacing between glass elements:
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
GlassChip(icon: "pencil")
GlassChip(icon: "eraser")
GlassChip(icon: "trash")
}
}
Note: The container's spacing parameter should match the actual spacing in your layout for proper glass effect rendering.
Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
Built-in button styles for glass appearance:
// Standard glass button
Button("Action") { }
.buttonStyle(.glass)
// Prominent glass button (higher visibility)
Button("Primary Action") { }
.buttonStyle(.glassProminent)
For more control, apply glass effect manually:
Button(action: { }) {
Label("Settings", systemImage: "gear")
.padding()
}
.glassEffect(.regular.interactive(), in: .capsule)
Create smooth transitions between glass elements using glassEffectID and @Namespace:
struct MorphingExample: View {
@Namespace private var animation
@State private var isExpanded = false
var body: some View {
GlassEffectContainer {
if isExpanded {
ExpandedCard()
.glassEffect()
.glassEffectID("card", in: animation)
} else {
CompactCard()
.glassEffect()
.glassEffectID("card", in: animation)
}
}
.animation(.smooth, value: isExpanded)
}
}
glassEffectID@NamespaceGlassEffectContainerCritical: Apply glassEffect after layout and visual modifiers:
// CORRECT order
Text("Label")
.font(.headline) // 1. Typography
.foregroundStyle(.primary) // 2. Color
.padding() // 3. Layout
.glassEffect() // 4. Glass effect LAST
// WRONG order - glass applied too early
Text("Label")
.glassEffect() // Wrong position
.padding()
.font(.headline)
struct GlassToolbar: View {
var body: some View {
if #available(iOS 26, *) {
GlassEffectContainer(spacing: 16) {
HStack(spacing: 16) {
ToolbarButton(icon: "pencil", action: { })
ToolbarButton(icon: "eraser", action: { })
ToolbarButton(icon: "scissors", action: { })
Spacer()
ToolbarButton(icon: "square.and.arrow.up", action: { })
}
.padding(.horizontal)
}
} else {
// Fallback toolbar
HStack(spacing: 16) {
// ... fallback implementation
}
}
}
}
struct ToolbarButton: View {
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title2)
.frame(width: 44, height: 44)
}
.glassEffect(.regular.interactive(), in: .circle)
}
}
struct GlassCard: View {
let title: String
let subtitle: String
var body: some View {
if #available(iOS 26, *) {
cardContent
.glassEffect(.regular, in: .rect(cornerRadius: 20))
} else {
cardContent
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
}
}
private var cardContent: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct GlassSegmentedControl: View {
@Binding var selection: Int
let options: [String]
@Namespace private var animation
var body: some View {
if #available(iOS 26, *) {
GlassEffectContainer(spacing: 4) {
HStack(spacing: 4) {
ForEach(options.indices, id: \.self) { index in
Button(options[index]) {
withAnimation(.smooth) {
selection = index
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.glassEffect(
selection == index ? .prominent.interactive() : .regular.interactive(),
in: .capsule
)
.glassEffectID(selection == index ? "selected" : "option\(index)", in: animation)
}
}
.padding(4)
}
} else {
Picker("Options", selection: $selection) {
ForEach(options.indices, id: \.self) { index in
Text(options[index]).tag(index)
}
}
.pickerStyle(.segmented)
}
}
}
if #available(iOS 26, *) {
content.glassEffect()
} else {
content.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
.ultraThinMaterial - Closest to glass appearance.thinMaterial - Slightly more opaque.regularMaterial - Standard blur.thickMaterial - More opaque.ultraThickMaterial - Most opaqueextension View {
@ViewBuilder
func glassEffectWithFallback(
_ style: GlassEffectStyle = .regular,
in shape: some Shape = .rect,
fallbackMaterial: Material = .ultraThinMaterial
) -> some View {
if #available(iOS 26, *) {
self.glassEffect(style, in: shape)
} else {
self.background(fallbackMaterial, in: shape)
}
}
}
In the new design, toolbar icons use monochrome rendering by default. The monochrome palette reduces visual noise and maintains legibility. Use tint(_:) only to convey meaning (e.g., a call to action), not for visual effect.
Partial-height sheets use a Liquid Glass background by default. If you previously used presentationBackground(_:) with a custom background, consider removing it to let the new material shine. Sheets can morph out of the glass controls that present them using navigationZoomTransition.
An automatic scroll edge effect blurs and fades content under system toolbars to keep controls legible. Remove any custom background-darkening effects behind bar items, as they will interfere.
Source: "Build a SwiftUI app with the new design" (WWDC25, session 323)
GlassEffectContainer for grouped glass elements (glass cannot sample other glass).interactive() only on tappable elementspresentationBackground(_:) on sheets to use the default glass material.interactive() on static contentGlassEffectContainer unnecessarily#available(iOS 26, *) with fallbackGlassEffectContainer wraps grouped elements.glassEffect() applied after layout modifiers.interactive() only on user-interactable elementsglassEffectID with @Namespace for morphing