Apple's introduction of Liquid Glass at WWDC 2025 is, without exaggeration, the most dramatic visual overhaul to iOS since the flat-design revolution of iOS 7. Starting with iOS 26, every system control — from tab bars and toolbars to navigation elements — adopts a translucent, light-bending material that dynamically responds to background content, device motion, and user interaction. The good news? SwiftUI ships with a clean, declarative API that lets you bring this exact same effect to your own custom views.
In this guide, we'll walk through every facet of the Liquid Glass API in SwiftUI: the core modifier and its configuration options, shape customization, tinting, interactive behaviors, container management, morphing transitions, accessibility, and real-world implementation patterns. Whether you're building a brand new iOS 26 app or modernizing an existing project, this should be the reference you keep coming back to.
Understanding Liquid Glass: More Than a Visual Gimmick
Before we jump into code, let's talk about what makes Liquid Glass different from previous translucent effects like .ultraThinMaterial or BlurEffect. Liquid Glass isn't just frosted glass — it's a dynamic material with several distinguishing characteristics that honestly caught me off guard when I first saw it in action:
- Real-time light bending (lensing): Content behind the glass element subtly distorts, creating a physical lens effect that shifts as the user scrolls or the glass moves. This is computed in real time by the GPU and can't be replicated with simple blur operations.
- Specular highlights: The glass surface responds to the device's gyroscope, producing light reflections that shift as the user tilts their phone. It creates a sense of physical materiality that flat translucent effects just can't match.
- Adaptive shadows: Drop shadows adjust dynamically based on the content beneath, maintaining visual hierarchy without hardcoded shadow values. The system automatically computes appropriate shadow opacity and spread.
- Vibrant text rendering: Text and icons placed on glass surfaces automatically get enhanced contrast and saturation to ensure readability against any background. The system adjusts color, brightness, and weight as the background changes.
- Interactive feedback: Glass elements can scale, bounce, shimmer, and illuminate at the touch point when users interact with them — providing rich haptic-like visual feedback.
All of this is handled by the system at the render layer. You don't manage shaders, track device orientation, or compute blur radii. SwiftUI does the heavy lifting through a single view modifier and a handful of supporting APIs.
The Core API: .glassEffect()
Everything starts with the glassEffect(_:in:isEnabled:) view modifier. Here's its full signature:
func glassEffect<S: Shape>(
_ glass: Glass = .regular,
in shape: S = DefaultGlassEffectShape,
isEnabled: Bool = true
) -> some View
At its simplest, you can apply it with no arguments at all:
Text("Hello, Liquid Glass!")
.padding()
.glassEffect()
That's it. This applies the default .regular glass variant in the default capsule shape. The text automatically receives vibrant rendering — the system adjusts its color, brightness, and saturation to maintain readability regardless of what sits behind the glass.
The Three Glass Variants
The Glass type provides three predefined material variants, each designed for specific use cases:
.regular— The standard glass material used across the system for toolbars, tab bars, and floating controls. It provides medium transparency with full adaptive behavior, responding to background content, lighting, and device motion. This is your default choice for the vast majority of glass use cases..clear— A highly transparent variant designed for situations where the content behind the glass should remain prominently visible. It provides increased transparency compared to regular, but applies a subtle dimming to the background to maintain readability of foreground elements..identity— Effectively disables the glass effect. This variant is invaluable for conditional glass application — you can toggle between.regularand.identitybased on state without restructuring your view hierarchy.
The .regular variant is your workhorse. Use it for the vast majority of floating UI elements.
The .clear variant is more specialized — Apple recommends it only when all three of these conditions are met:
- The glass element sits over media-rich content (photos, video, maps)
- The content beneath won't suffer from the subtle dimming that clear glass applies
- The content displayed on top of the glass is bold and visually prominent
Here's how to use the .identity variant for conditional glass:
struct AdaptiveButton: View {
@State private var useGlass = true
var body: some View {
Button("Settings") {
// action
}
.padding()
.glassEffect(useGlass ? .regular : .identity)
}
}
Shape Customization
The second parameter of .glassEffect() controls the shape of the glass boundary. By default, it uses a capsule, but you can pass any SwiftUI Shape. This flexibility is great — you're not locked into rounded rectangles. The glass effect faithfully follows whatever contour you provide.
// Circular glass for icon buttons
Image(systemName: "heart.fill")
.font(.title2)
.padding(12)
.glassEffect(.regular, in: .circle)
// Rounded rectangle for cards
VStack {
Text("Notification")
.font(.headline)
Text("You have 3 new messages")
.font(.subheadline)
}
.padding()
.glassEffect(.regular, in: .rect(cornerRadius: 16))
// Capsule (the default, but explicit here)
Label("Share", systemImage: "square.and.arrow.up")
.padding(.horizontal, 20)
.padding(.vertical, 10)
.glassEffect(.regular, in: .capsule)
// Ellipse for wider, flatter controls
Text("Now Playing")
.padding(.horizontal, 30)
.padding(.vertical, 8)
.glassEffect(.regular, in: .ellipse)
Container-Concentric Corner Radii
iOS 26 introduces a smart corner radius option that automatically matches the curvature of a parent container:
.glassEffect(.regular, in: .rect(cornerRadius: .containerConcentric))
This is especially useful when nesting glass elements within other glass containers. The inner element's corners automatically adjust to create that visually harmonious concentric radius pattern Apple uses throughout the system. The inner radius is mathematically computed to look proportional to the outer radius, so you don't have to guess at nested corner values anymore.
Custom Shapes
Since the shape parameter accepts any Shape conformance, you can create entirely custom glass silhouettes:
struct DiamondShape: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
let mid = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: mid.x, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: mid.y))
path.addLine(to: CGPoint(x: mid.x, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: mid.y))
path.closeSubpath()
}
}
}
// Usage
Image(systemName: "star.fill")
.font(.largeTitle)
.padding(24)
.glassEffect(.regular, in: DiamondShape())
One thing to keep in mind: complex custom shapes with many path segments may impact rendering performance, since the glass effect needs to compute lensing and reflections along the entire boundary. For best results, keep custom glass shapes relatively simple with smooth curves.
Tinting: Adding Semantic Color
The .tint() modifier on the Glass type blends a color into the glass material. This lets you convey semantic meaning or brand identity without losing the translucent character:
// Solid tint for strong semantic signals
Text("Error")
.padding()
.glassEffect(.regular.tint(.red))
// Tint with reduced opacity for subtlety
Text("Info")
.padding()
.glassEffect(.regular.tint(.blue.opacity(0.6)))
// Tinted clear glass over media
Image(systemName: "camera.fill")
.padding()
.glassEffect(.clear.tint(.purple.opacity(0.4)))
// Brand-colored action button
Button("Subscribe") { }
.padding(.horizontal, 24)
.padding(.vertical, 12)
.glassEffect(.regular.tint(.orange.opacity(0.5)))
Tinting works with all glass variants and composes naturally with other modifiers. The system intelligently blends your tint color with the underlying glass material, so the result always looks physically plausible rather than like a flat color overlay. The tint subtly shifts in hue and saturation as the background content changes, which is a really nice touch that maintains the characteristic Liquid Glass dynamism.
Interactive Glass
The .interactive() modifier transforms passive glass elements into rich, responsive controls. When enabled, the glass surface gains several behaviors that provide haptic-like visual feedback without you needing custom gesture recognizers or animations:
- Scale response: The element slightly grows on press and bounces back on release with a spring animation
- Shimmer effect: A subtle light shimmer sweeps across the glass during interaction, simulating light catching a physical surface
- Touch-point illumination: The glass brightens right where the user's finger contacts the surface — it's surprisingly satisfying
- Gesture responsiveness: Drag and long-press gestures produce continuous visual feedback as the finger moves across the glass
- Enhanced background sampling: Interactive glass is more aggressively responsive to background content changes, creating a more dynamic visual experience
// Interactive button with tint
Button(action: { /* perform action */ }) {
Label("Download", systemImage: "arrow.down.circle.fill")
.font(.headline)
.padding(.horizontal, 24)
.padding(.vertical, 12)
}
.glassEffect(.regular.interactive().tint(.green))
// Modifier composition — order is irrelevant
.glassEffect(.regular.tint(.orange).interactive())
.glassEffect(.clear.interactive().tint(.blue))
Important performance note: Only enable .interactive() on elements that are actually tappable or draggable. Applying it to passive, non-interactive content adds rendering cost through continuous gesture tracking and animation updates without providing any benefit to the user.
The Built-in Glass Button Style
For standard buttons, SwiftUI provides a convenience .glass button style that wraps the common pattern:
Button("Continue") {
// action
}
.buttonStyle(.glass)
This is subtly different from manually applying .glassEffect(.regular.interactive()) to a button. The built-in style handles padding, hit testing, and animation curves in a way that's specifically optimized for the button interaction model. It also integrates with the system's button role semantics, so a .destructive role button with glass styling will automatically pick up the appropriate red tint.
Use .buttonStyle(.glass) for standard buttons and fall back to manual .glassEffect() only when you need custom sizing, shape, or composition.
GlassEffectContainer: Coordinating Multiple Glass Elements
When you have multiple glass elements in proximity, you need to coordinate them using GlassEffectContainer. This isn't optional — it's a rendering requirement. Here's why:
Glass cannot sample other glass. If two glass elements overlap or sit near each other in separate containers, they'll produce inconsistent or broken visual results because each element tries to sample the content behind it, which might include the other glass element's distortion. GlassEffectContainer solves this by establishing a shared sampling region that all enclosed glass elements draw from.
GlassEffectContainer {
HStack(spacing: 12) {
Button(action: {}) {
Image(systemName: "backward.fill")
.font(.title2)
}
.padding(12)
.glassEffect(.regular, in: .circle)
Button(action: {}) {
Image(systemName: "play.fill")
.font(.title)
}
.padding(16)
.glassEffect(.regular.interactive(), in: .circle)
Button(action: {}) {
Image(systemName: "forward.fill")
.font(.title2)
}
.padding(12)
.glassEffect(.regular, in: .circle)
}
}
Container Spacing and Automatic Merging
GlassEffectContainer accepts a spacing parameter that controls the distance threshold for visual merging. When glass elements within the container are positioned closer than this threshold, they visually merge into a single continuous glass surface:
// Elements within 40 points of each other will merge
GlassEffectContainer(spacing: 40.0) {
VStack(spacing: 8) {
ForEach(menuItems) { item in
Button(item.title) { item.action() }
.padding()
.glassEffect(.regular, in: .capsule)
}
}
}
This merging behavior is what creates the fluid, connected toolbar look you see throughout iOS 26. Elements close together blend into a unified glass surface with shared reflections and shadows, while elements farther apart remain visually distinct. Adjusting the spacing parameter lets you fine-tune exactly when elements merge versus stay separate — it's a small detail that makes a big visual difference.
Morphing Transitions with glassEffectID
Okay, this is where things get really fun. One of the most visually impressive capabilities of the Liquid Glass system is morphing — smooth shape transitions between glass elements as they appear, disappear, or change position. This is enabled through the glassEffectID(_:in:) modifier combined with a @Namespace.
How Morphing Works
The morphing system requires four ingredients working together:
- All participating glass elements must share the same
GlassEffectContainer - Each glass element receives a unique identifier via
.glassEffectID(_:in:) - Identifiers are scoped to a namespace created with the
@Namespaceproperty wrapper - State changes wrapped in
withAnimationtrigger the morphing animations when views appear, disappear, or reposition
struct ExpandableToolbar: View {
@State private var isExpanded = false
@Namespace private var toolbarNamespace
var body: some View {
GlassEffectContainer(spacing: 30) {
HStack(spacing: 12) {
Button {
withAnimation(.bouncy) {
isExpanded.toggle()
}
} label: {
Image(systemName: isExpanded ? "xmark" : "plus")
.font(.title2)
.contentTransition(.symbolEffect(.replace))
}
.padding(14)
.glassEffect(.regular.interactive(), in: .circle)
.glassEffectID("toggle", in: toolbarNamespace)
if isExpanded {
Button {
// camera action
} label: {
Image(systemName: "camera.fill")
.font(.title3)
}
.padding(12)
.glassEffect(.regular.interactive(), in: .circle)
.glassEffectID("camera", in: toolbarNamespace)
.transition(.blurReplace)
Button {
// photo library action
} label: {
Image(systemName: "photo.on.rectangle")
.font(.title3)
}
.padding(12)
.glassEffect(.regular.interactive(), in: .circle)
.glassEffectID("photos", in: toolbarNamespace)
.transition(.blurReplace)
Button {
// document action
} label: {
Image(systemName: "doc.fill")
.font(.title3)
}
.padding(12)
.glassEffect(.regular.interactive(), in: .circle)
.glassEffectID("document", in: toolbarNamespace)
.transition(.blurReplace)
}
}
}
}
}
When the user taps the plus button, the additional action buttons emerge with a fluid morphing animation — the glass surfaces expand and separate from the main button, creating a cohesive transition that feels like the buttons are being extruded from the original element. On collapse, the process reverses as the buttons merge back into the toggle. It's one of those things that looks complex but is surprisingly simple to implement.
Removing Backgrounds for Glass
A common mistake when adopting Liquid Glass (and I've definitely made it myself) is leaving explicit background modifiers on views. Because glass isn't a background — it's a separate rendering layer that sits atop your content hierarchy — existing backgrounds will interfere with the effect and block the glass from sampling the actual content behind it:
// Wrong — the background fights with the glass
Text("Action")
.padding()
.background(.blue)
.glassEffect()
// Correct — let the glass be the visual treatment
Text("Action")
.padding()
.glassEffect(.regular.tint(.blue))
If you need a colored treatment, use the .tint() modifier on the glass variant instead of a separate background view. This ensures the color blends naturally with the glass material and maintains the dynamic lensing behavior.
Where Glass Belongs (and Where It Doesn't)
Apple's design guidelines are pretty firm on this: Glass is exclusively for the navigation layer that floats above app content. Understanding this distinction is critical to creating apps that feel native on iOS 26.
Appropriate Uses
- Toolbars and navigation bars
- Tab bars
- Floating action buttons
- Media playback controls
- Overlay sheets and popovers
- Contextual menus and action sheets
- Status indicators that float over content
- Bottom bar controls and safe area insets
Inappropriate Uses
- List rows or table cells
- Card layouts within scrollable content
- Form fields or text inputs
- Content backgrounds or section headers
- Profile cards or information displays
- Every element in your UI (resist the temptation!)
Applying glass to content-level elements creates visual noise — the distortion and reflections compete with readability, and the constant lensing effect makes scrolling feel chaotic rather than fluid. Remember: glass creates depth by contrast with non-glass content. If everything is glass, nothing stands out.
Building a Real-World Example: A Media Player Overlay
So, let's put everything together in a practical example. Here's a floating media player control that appears over photo content, demonstrating tinting, interactive glass, morphing transitions, and container management all working in concert:
struct MediaPlayerOverlay: View {
@State private var isPlaying = false
@State private var showQueue = false
@Namespace private var playerNamespace
var body: some View {
ZStack(alignment: .bottom) {
// Background content — a photo grid
ScrollView {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 100))],
spacing: 4
) {
ForEach(0..<50) { index in
Rectangle()
.fill(
Color(
hue: Double(index) / 50.0,
saturation: 0.7,
brightness: 0.8
)
)
.aspectRatio(1, contentMode: .fill)
}
}
.padding(.horizontal, 4)
.padding(.bottom, 120)
}
// Floating glass player controls
GlassEffectContainer(spacing: 24) {
VStack(spacing: 12) {
if showQueue {
VStack(spacing: 8) {
ForEach(
["Next: Solar Winds",
"After: Ocean Deep",
"Later: Mountain Echo"],
id: \.self
) { track in
HStack {
Image(systemName: "music.note")
Text(track)
.font(.subheadline)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.glassEffect(.regular, in: .capsule)
.glassEffectID(
track, in: playerNamespace
)
}
}
.transition(.blurReplace)
}
HStack(spacing: 16) {
Button {
withAnimation(.bouncy) {
showQueue.toggle()
}
} label: {
Image(systemName: "list.bullet")
.font(.title3)
}
.padding(10)
.glassEffect(
.regular.interactive(), in: .circle
)
.glassEffectID(
"queue", in: playerNamespace
)
VStack(spacing: 2) {
Text("Solar Winds")
.font(.subheadline)
.fontWeight(.semibold)
Text("Aurora Project")
.font(.caption)
.foregroundStyle(.secondary)
}
HStack(spacing: 12) {
Button {} label: {
Image(
systemName: "backward.fill"
)
}
Button {
isPlaying.toggle()
} label: {
Image(
systemName: isPlaying
? "pause.fill"
: "play.fill"
)
.contentTransition(
.symbolEffect(.replace)
)
}
.font(.title2)
Button {} label: {
Image(
systemName: "forward.fill"
)
}
}
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
.glassEffect(
.regular.tint(.indigo.opacity(0.3)),
in: .capsule
)
.glassEffectID(
"player", in: playerNamespace
)
}
.padding(.horizontal, 20)
.padding(.bottom, 30)
}
}
}
}
This example pulls together several key patterns: the main player bar uses tinted glass to establish visual identity, the queue button is a separate circular glass element with interactive feedback, the queue list items use individual glass effects with morphing IDs, everything is wrapped in a GlassEffectContainer for proper rendering, and state changes drive morphing animations via withAnimation(.bouncy).
Accessibility: The System Has Your Back
One of the best aspects of the Liquid Glass API is that accessibility adaptations happen automatically. You don't need to write a single extra line of code for these system-wide settings:
- Reduce Transparency: When users enable this setting, the glass frosting becomes more opaque and less distracting, ensuring readability for users with visual impairments. The lensing effect is reduced or eliminated.
- Increase Contrast: Glass elements gain more pronounced borders and stronger color differentiation between the glass surface and surrounding content.
- Reduce Motion: The parallax effects, specular highlight shifts, and morphing animations are toned down or eliminated entirely.
- Tinted Mode (iOS 26.1+): Users can control the overall glass opacity system-wide from Settings. Your code doesn't need to change — the system adjusts rendering automatically.
In rare cases where you need manual control over accessibility behavior, you can query the environment:
struct AccessibilityAwareGlass: View {
@Environment(\.accessibilityReduceTransparency)
private var reduceTransparency
var body: some View {
Label("Action", systemImage: "star.fill")
.padding()
.glassEffect(
reduceTransparency ? .identity : .regular
)
}
}
That said, Apple strongly recommends against manually overriding accessibility behavior. The system's automatic adaptations are well-tuned and cover edge cases you might not think of. Only override when you have a specific, tested reason to do so.
Performance Best Practices
Liquid Glass is GPU-intensive by nature — the real-time lensing, specular highlights, and adaptive shadows all require significant rendering resources. Here are the guidelines that'll keep your app performant across all device tiers.
1. Use GlassEffectContainer for Grouping
Always wrap multiple nearby glass elements in a container. Beyond preventing visual artifacts, the container allows the system to share a single sampling region across all elements, which dramatically reduces the number of render passes needed.
2. Limit Glass Element Count
Avoid creating dozens of glass elements in a single view. If you're rendering a list of items, glass should be on the overlay controls — not on every cell. A good rule of thumb: fewer than 10 glass elements visible on screen at any time. Beyond that, you'll start seeing frame drops on older devices.
3. Reserve .interactive() for Tappable Elements
Interactive glass adds gesture tracking, continuous animation updates, and touch-point highlighting. That's wasted computation on passive labels or decorative elements. Only apply .interactive() to buttons, toggles, and other controls the user will actually tap.
4. Prefer .regular Over .clear
The clear variant requires more aggressive background sampling to maintain readability because it allows more content to show through. Use it only when the design genuinely demands high transparency over media content.
5. Conditional Application During Rapid Scrolling
If glass elements appear in a scrollable context (say, in a safeAreaInset), consider disabling glass or switching to .identity during rapid scrolling to maintain frame rate on lower-end devices:
struct SmartGlassOverlay: View {
@State private var isScrolling = false
var body: some View {
ScrollView {
// content
}
.safeAreaInset(edge: .bottom) {
PlayerControls()
.glassEffect(
isScrolling ? .identity : .regular
)
}
}
}
6. Centralize Glass Configuration
Rather than sprinkling .glassEffect() with different parameters all over your codebase, create shared view modifiers that encapsulate your app's glass design tokens. This ensures visual consistency and makes it trivial to update your glass styling across the entire app:
extension View {
func primaryGlass() -> some View {
self.glassEffect(
.regular.interactive().tint(.blue.opacity(0.2))
)
}
func secondaryGlass() -> some View {
self.glassEffect(.regular)
}
func iconGlass() -> some View {
self.glassEffect(.regular.interactive(), in: .circle)
}
}
// Usage becomes clean and consistent
Button("Save") { }
.padding()
.primaryGlass()
Image(systemName: "gear")
.padding(12)
.iconGlass()
Migrating Existing UI to Liquid Glass
If you're updating an existing iOS app for iOS 26, here's a practical step-by-step migration strategy that should minimize risk while progressively adopting the new design language.
Step 1: Identify Navigation-Layer Elements
Audit your app for elements that float above content: custom toolbars, floating action buttons, overlay controls, bottom sheets, and player bars. These are your primary candidates for glass treatment.
Step 2: Remove Explicit Backgrounds
Replace .background() modifiers on navigation-layer elements with glass effects. If the background had a specific color, use .tint() to preserve the semantic meaning:
// Before
HStack { /* toolbar content */ }
.padding()
.background(.ultraThinMaterial, in: .capsule)
// After
HStack { /* toolbar content */ }
.padding()
.glassEffect(.regular, in: .capsule)
Step 3: Wrap Related Glass Elements
Group nearby glass elements into GlassEffectContainer instances. Think in terms of logical groups: all the controls in a toolbar, all the tabs in a tab bar, all the buttons in a media player.
Step 4: Add Morphing for State Transitions
If glass elements appear or disappear based on state changes (expanding menus, showing/hiding controls), add .glassEffectID() modifiers with a shared namespace to get those smooth morphing transitions users will expect on iOS 26.
Step 5: Test Across Accessibility Settings
Verify that your glass implementations look correct with Reduce Transparency, Increase Contrast, and Reduce Motion enabled. The system handles most of this automatically, but custom configurations might need manual testing.
Common Mistakes and How to Avoid Them
After spending a lot of time with Liquid Glass, several anti-patterns keep coming up again and again. Here are the most common ones and how to fix them.
Mistake 1: Applying Glass to Content
// Don't do this — glass on every list row
List {
ForEach(items) { item in
Text(item.name)
.padding()
.glassEffect()
}
}
// Instead, use glass only on floating overlays
List {
ForEach(items) { item in
Text(item.name)
}
}
.safeAreaInset(edge: .bottom) {
ActionBar()
.glassEffect(.regular, in: .capsule)
}
Mistake 2: Overlapping Glass Without a Container
// Don't do this — glass elements can't sample each other
ZStack {
Text("Layer 1").glassEffect()
Text("Layer 2").glassEffect()
}
// Do this — container provides shared sampling
GlassEffectContainer {
ZStack {
Text("Layer 1").glassEffect()
Text("Layer 2").glassEffect()
}
}
Mistake 3: Mixing Background and Glass
// Don't do this — background blocks glass sampling
Text("Action")
.background(.blue)
.glassEffect()
// Do this — use tint for colored glass
Text("Action")
.glassEffect(.regular.tint(.blue))
Mistake 4: Making Everything Interactive
// Don't do this — passive labels don't need interactivity
Text("Status: Online")
.glassEffect(.regular.interactive())
// Do this — reserve interactive for tappable elements
Text("Status: Online")
.glassEffect(.regular)
Looking Forward
Liquid Glass is clearly the design direction Apple is committed to for the foreseeable future. As iOS 26.x updates continue to refine the rendering pipeline, we can expect improved performance on older hardware, more configuration options, and deeper integration with other SwiftUI features like matched geometry effects and custom transitions.
The key takeaway? Start adopting Liquid Glass now, but adopt it thoughtfully. Use it where it makes physical sense — controls that float above content, navigation elements that need to be visible but not visually dominant. Resist the urge to glass everything, and your apps will look stunning without sacrificing performance or accessibility.
The API surface is deliberately small and opinionated. Apple wants you to use .glassEffect() on the right elements in the right places, let the system handle the complex rendering, and focus your energy on building great app experiences. Honestly, that's the best kind of API — one that makes the right thing easy and the wrong thing hard.