| name | liquid-glass |
| description | Design and implement Apple's Liquid Glass — the iOS/iPadOS/macOS 26 design material — across SwiftUI, UIKit, AppKit, and WidgetKit. Use when adopting iOS 26 design, adding glass/blur effects, building floating chrome or morphing toolbars, or migrating an app to the iOS 26 SDK. Triggers: "Liquid Glass", "glassEffect", ".buttonStyle(.glass)", "GlassEffectContainer", "glassEffectID", "UIGlassEffect", "NSGlassEffectView", "iOS 26 design", "tabBarMinimizeBehavior", "scrollEdgeEffectStyle", "ToolbarSpacer", "safeAreaBar", "concentric corners", "frosted glass UI", "migrate to iOS 26", "UIDesignRequiresCompatibility", "glass widget", "backgroundExtensionEffect", "tabViewBottomAccessory". |
Liquid Glass — iOS 26 Design Material
Liquid Glass is the primary UI material introduced in iOS 26 / iPadOS 26 /
macOS Tahoe (26) / watchOS 26 / visionOS 26. It is a dynamic, translucent
material that refracts and reflects its surroundings, forming a distinct
functional layer for controls and navigation that floats above content.
The single most important rule: most of the time you write no glass code at
all. Standard SwiftUI / UIKit components adopt Liquid Glass automatically when
you recompile against the iOS 26 SDK. The .glassEffect() family is reserved
for custom floating chrome — not content, not whole screens.
Quick mental model
| Layer | What lives here | Glass? |
|---|
| Content layer | Lists, text, images, the actual data | NEVER apply glass here |
| Functional layer | Toolbars, tab/nav bars, sheets, floating buttons, custom controls | Glass — usually automatic, occasionally .glassEffect() |
Putting .glassEffect() on a List, a ScrollView, or a full-screen background
is the anti-pattern this whole material is designed to prevent.
Design rules (apply these before writing any code)
- Use standard components.
NavigationStack, TabView, toolbars, sheets,
popovers, alerts, Toggle, Slider, Stepper, segmented Picker — all
auto-adopt Liquid Glass. Don't rebuild them.
- Glass is for the functional layer only — navigation and controls that
float above content. Never the content layer.
- Use it sparingly on custom views. Limit
.glassEffect() to the most
important functional elements — a screen full of glass reads as noise.
- Remove custom bar backgrounds. Delete
.toolbarBackground(Color…),
presentationBackground on sheets, and any hand-rolled .ultraThinMaterial
bar backgrounds — let the system render the bar.
- Don't layer glass on glass. Glass cannot sample glass — stacking two
.glassEffect() views renders muddy; group them in a GlassEffectContainer or
reduce to one.
- Maintain legibility under bars with
.scrollEdgeEffectStyle(...). It's
not decorative — apply only where a scroll view is adjacent to floating
glass controls. One per view edge; never mix/stack soft + hard on the same
edge; in split views each pane may have its own (consistent heights). .soft
= iOS/iPadOS default; .hard = macOS default (stronger boundary, for
interactive text / pinned headers).
- Prefer system button styles —
.buttonStyle(.glass) /
.buttonStyle(.glassProminent) over hand-built glass buttons.
- Match hardware curvature with
ConcentricRectangle /
.containerConcentric rather than guessing corner radii.
- Test with accessibility settings: Reduce Transparency, Increase Contrast,
Reduce Motion — glass respects all three automatically; verify your layout
still works when glass goes opaque.
- Glass is size-adaptive. Small elements (nav/tab bars, buttons) flip
light/dark with their background; large ones (menus, sidebars, sheets) adapt
but don't flip (too much surface area). On Mac/iPad, glass recedes when the
window loses focus. Don't fight it — a tab bar and a sheet over the same
content looking different is correct.
The three glass variants
| Variant | API | When |
|---|
| Regular | .glassEffect(.regular) (default) | ~95% opaque. The default for chrome, toolbars, cards, alerts, sidebars, popovers. Adjusts luminosity for legibility. |
| Clear | .glassEffect(.clear) | Highly translucent, no light/dark switching. Use ONLY when all three hold: (1) over media-rich content, (2) a dimming layer won't harm it, (3) content on top is bold and bright. Add a 35% dark dimming layer beneath if the background is bright. Never mix Regular and Clear in one context. |
| Identity | .glassEffect(.identity) | No visual effect, view stays in place. For conditionally disabling glass without an if/else (which would break view identity/animations). |
Modifiers chain on any variant — .tint(Color) (accent for prominent/selected
state) and .interactive() (touch/pointer feedback on buttons & custom controls):
e.g. .glassEffect(.regular.tint(.blue).interactive(), in: .circle).
To toggle glass off for state-driven reasons, use the variant form
.glassEffect(showGlass ? .regular : .identity) or the parameter form
.glassEffect(.regular, isEnabled: showGlass) — never an if/else (it breaks
view identity). You do not need this for accessibility (glass respects Reduce
Transparency automatically); a manual accessibilityReduceTransparency override is
a last resort, in references/swiftui-advanced-and-gotchas.md.
SwiftUI — the essentials
Basic glass + shapes
.glassEffect() applies Capsule glass to a custom view; pass a shape with in: —
.glassEffect(in: .capsule) (default), .glassEffect(in: .rect(cornerRadius: 16)),
or .glassEffect(in: .circle). Apply .glassEffect() LAST in the chain — after
.frame, .padding, and content modifiers.
Multiple glass elements — GlassEffectContainer
Required whenever 2+ glass shapes sit near each other — it renders one combined
blur/shadow/specular pass (performance) and enables morphing.
GlassEffectContainer(spacing: 20.0) {
HStack(spacing: 20.0) {
Image(systemName: "star.fill").frame(width: 80, height: 80).glassEffect()
Image(systemName: "heart.fill").frame(width: 80, height: 80).glassEffect()
}
}
spacing controls when effects merge (smaller = views must be closer to blend).
Only wrap the spatially-related cluster, never the whole screen.
Why it matters: each .glassEffect() = one CABackdropLayer × 3 offscreen
textures (N effects = N×3 = the real source of frame drops); a container shares
one sampling region. Element ceilings: iPhone 4–6, iPad 6–8, macOS 8–10, watchOS
1–2, tvOS 4–6, visionOS 6–8. In long lists put glass on a floating .overlay, not
on LazyVStack rows; conform rarely-changing glass views to Equatable; size the
inner content, not the container.
Morphing transitions — @Namespace + glassEffectID
Tag glass shapes inside a GlassEffectContainer with a stable glassEffectID in a
shared @Namespace, then toggle them inside withAnimation — the container morphs
between states. .glassEffectUnion(id:in:) fuses shapes into one continuous blob
(icon + label as a single pill); different union ids inside a ForEach make
separate blobs. Full ExpandableToolbar example in
references/swiftui-advanced-and-gotchas.md.
The container's transition is selectable via GlassEffectTransition:
.matchedGeometry (default) for shapes that move, .materialize for shapes
that appear/disappear, .identity for none. The exact modifier spelling
varies across sources (GlassEffectTransition.materialize vs
.glassEffect(.materialize)) — verify against the SDK; semantics are stable.
Details in references/swiftui-advanced-and-gotchas.md.
Known morph bug: writing observable state from .onGeometryChange anywhere
in a TabView subtree breaks the Tab(role: .search) morph on its first
activation (search field appears as a separate top bar). Seed window-size
@State once instead, or move the observer outside the TabView's coordinate
space. Reproduced iOS 26.0; fix in references/swiftui-advanced-and-gotchas.md.
System button styles
Button("Cancel") { }.buttonStyle(.glass)
Button("Confirm") { }.buttonStyle(.glassProminent)
iOS 26.1+ adds GlassButtonStyle(.clear / .glass / .tint) and
.buttonSizing(.fit / .stretch / .flexible) for layout. Bordered buttons now
default to capsule — override with .buttonBorderShape(.roundedRectangle).
Media overlay (Clear variant)
A media control is just a button with .glassEffect(.clear.interactive(), in: .circle) over the video/photo. When the media is bright, put a
Color.black.opacity(0.25–0.35) dimming layer under the glass in a ZStack (not
.opacity() on the glass itself) so foreground glyphs stay legible.
Background extension — content behind the bars
.backgroundExtensionEffect() on a hero image makes it read as if it extends
behind the bars by generating a mirrored + blurred replica outside the safe
area (not a stretch). Legibility-gradient and parent-clipping recipes are in
references/swiftui-advanced-and-gotchas.md; UIKit UIBackgroundExtensionView
(sibling-not-subview gotcha) + AppKit NSBackgroundExtensionView in
references/uikit-appkit-widgetkit.md.
Tab bar & scroll-edge behavior
TabView {
Tab("Home", systemImage: "house") { … }
Tab(role: .search) { … }
}
.tabBarMinimizeBehavior(.onScrollDown)
A floating glass accessory docks just above the tab bar (now-playing / mini-player
slot) via .tabViewBottomAccessory { … }; read \.tabViewBottomAccessoryPlacement
from the environment to adjust its content when the bar collapses inline. (UIKit:
UITabAccessory + tabBarController.bottomAccessory.)
For a custom sticky bar with progressive blur (replacing manual material bars), use
.safeAreaBar(edge: .bottom) { … } on the scroll content plus
.scrollEdgeEffectStyle(.soft, for: .bottom) (or .hard).
iOS 26 toolbar APIs (high-value, frequently needed)
ToolbarSpacer(.fixed/.flexible, placement:) — separate or push toolbar groups
apart (e.g. between two ToolbarItem(placement: .bottomBar) buttons).
ToolbarItemGroup — items share one glass "pill"; confirmationAction
placement → prominent styling, cancellationAction → standard. Use
.sharedBackgroundVisibility(.hidden) to exclude an item (e.g. an avatar).
Don't mix text + icons in one group — image buttons share one background
while text/Done/prominent buttons get their own, so a mixed group reads as a
single combined button. Separate them with a ToolbarSpacer; monochrome icons
reduce visual noise.
- Toolbar morphing: attach
.toolbar {} to the views inside a
NavigationStack, NOT the NavigationStack itself — iOS 26 morphs per-view
toolbars across push/pop. Use .toolbar(id:) + matching ToolbarItem(id:) so
shared items stay stable. #1 gotcha: a toolbar on the stack has nothing to
morph between.
DefaultToolbarItem(kind: .search, placement: .bottomBar) — reposition the
system search field.
.searchToolbarBehavior(.minimize) — compact search button that expands on
tap.
- Zoom transition — make a sheet/detail morph out of the button that
presents it:
.matchedTransitionSource(id:in:) on the source +
.navigationTransition(.zoom(sourceID:in:)) on the destination. Critical: it
no-ops unless the destination is inside a NavigationStack/NavigationSplitView.
Full snippet (+ UIKit preferredTransition = .zoom) in
references/swiftui-advanced-and-gotchas.md.
UIKit, AppKit, WidgetKit
Full code lives in references/uikit-appkit-widgetkit.md. The one-line map:
| SwiftUI | UIKit | AppKit |
|---|
.glassEffect() | UIVisualEffectView(effect: UIGlassEffect()) | NSGlassEffectView |
.glassEffect(.regular.interactive()) | UIGlassEffect() + isInteractive = true | tracking-area + animate tintColor |
.glassEffect(.regular.tint(.blue)) | UIGlassEffect() + tintColor | glassView.tintColor |
GlassEffectContainer(spacing:) | UIGlassContainerEffect() + spacing | NSGlassEffectContainerView + spacing |
.buttonStyle(.glass) | insert UIVisualEffectView as button subview | — |
Migrating an existing app to iOS 26
Adoption is gated by SDK build, not deployment target — recompiling on the
iOS 26 SDK (Xcode 26) makes standard components adopt glass automatically. You do
not raise your deployment target; users are not forced onto iOS 26; shipped
apps are not removed. Two phases: Phase 1 build clean on the SDK (fix
deprecations, optionally set UIDesignRequiresCompatibility = true as a one-release
bridge); Phase 2 fully adopt (remove the flag, delete custom bar backgrounds,
swap .background(.ultraThinMaterial) chrome for .glassEffect(), adopt the new
toolbar/search/concentric APIs).
Full timeline, mechanics, pre-migration grep audit, deprecated-API table,
adoption lessons, reusable back-compat adaptiveGlass(in:), readiness checklist,
and the known iOS 26 UIKit gotchas (allowsLiquidTransform distortion,
floating-TabBar safe-area shifts, auto-inserted UIDropShadowView, reversed
rightBarButtonItems) are in references/migration-and-adoption.md.
Replacing the old materials API
Map manual material chrome to glass — but not every translucent layer becomes
glass: full-screen blur stays .background(.ultraThinMaterial), scrims stay
Color.black.opacity(...) (see the decision tree).
| Old approach | Liquid Glass |
|---|
.background(.material) + .cornerRadius(N) on chrome | .glassEffect(in: .rect(cornerRadius: N)) |
| no interactivity / tint | .interactive() / .tint(Color) |
| no morphing / grouping | glassEffectID + @Namespace / container |
When NOT to use glass (decision tree)
- Standard control (Button, Toggle, Slider, Stepper, Picker, Tab, Toolbar)?
→ use the standard control; glass is automatic. No
.glassEffect().
- Standard navigation (
TabView, NavigationStack toolbar)? → automatic.
- Interactive custom chrome (floating action, custom control)?
→
.glassEffect(.regular.interactive(), in: shape)
- Non-interactive floating panel / card?
→
.glassEffect(.regular, in: .rect(cornerRadius: N))
- Media overlay (video controls, photo viewer)?
→
.glassEffect(.clear, in: shape)
- Full-screen background blur? →
.background(.ultraThinMaterial) — NOT glass
- Multiple glass elements close together? → wrap in
GlassEffectContainer(spacing:)
Two more anti-patterns:
- Content intersecting glass at rest → reposition/scale the content
(
backgroundExtensionEffect is for dynamic scroll overlap, not static).
- Maps pattern → remove floating glass buttons when a glass sheet expands
(else it's glass-on-glass once the sheet rises).
Shadows, specular, and corners
- Glass renders its own shadow and specular highlight. Never add manual
.shadow() on top of .glassEffect() — it doubles the effect. If a Figma
spec shows a shadow on a glass element, the glass already provides it.
- Don't fake transparency with
.opacity() — switch .regular → .clear.
- Use
ConcentricRectangle / .clipShape(.rect(cornerRadius: N, style: .containerConcentric)) so corners stay concentric with the hardware. The radius
you pass is a fallback used only when the shape is standalone (nested, it
adapts to the container). Diagnostic: pinched/flared corners in a nested
container usually mean the inner shape itself needs to be concentric too.
- iOS 26 lists/forms have larger row height, more padding, more rounded section
corners, and title-case (not ALL-CAPS) section headers — don't fight the new
system metrics.
- App-icon backplate (cross-link
app-icons): default backplate = Icon
Composer fill/gradient metadata, not a baked PNG; a custom image background
must zero its opacity in clear/tinted modes so the system material shows.
Interaction & animation pitfalls (the ones that waste hours)
Frequently-hit traps for custom glass. Each has full code + a workaround in
references/swiftui-advanced-and-gotchas.md:
- Padded glass is dead to touch → add a matching
.contentShape(...).
.foregroundStyle(.white) on .glassProminent KILLS vibrancy (flat
painted-on glyphs); explicit .black for dark-mode legibility on a pale tint
is a different, allowed fix.
- Tinted glass that is NOT a
Button needs .interactive() or it feels dead.
Menu + glass morph artifact (rect→circle), and Menu in a
GlassEffectContainer breaks morphing on 26.1 — fixes diverge by release.
rotationEffect morphs the glass shape → bridge to UIKit UIGlassEffect.
.symbolEffect(.drawOn) crashes pre-iOS-26 → gate with if #available.
.glassProminent + .buttonBorderShape(.circle) artifact → trailing
.clipShape(Circle()).
- Custom glass button sizing: an outer
.frame(height:44) won't enlarge a
style-wrapped pill — build the label manually (reference).
Translating a Figma / design spec to glass
Map standard Figma components to standard SwiftUI controls (glass is free); reach
for .glassEffect() only on genuinely custom floating chrome. Full component
mapping + corner-radius/control-size tables in references/figma-to-glass.md.
References
| File | Content |
|---|
| references/swiftui-advanced-and-gotchas.md | SwiftUI worked examples + traps: morphing ExpandableToolbar, transition types, FlexibleHeader parallax hero, backgroundExtensionEffect fixes, zoom transitions, the search-tab/Menu/rotationEffect/.drawOn/contentShape gotchas, glass-button legibility & sizing traps, perf detail |
| references/uikit-appkit-widgetkit.md | UIKit (UIGlassEffect, animate via effect, glass UIButton.Configurations, container/scroll-edge, UIBackgroundExtensionView, configureWithDefaultBackground, action-sheet sourceItem, zoom, UITabAccessory, badges), AppKit (NSGlassEffectView, tintProminence, bezelStyle, LayoutRegion, split-view, NSBackgroundExtensionView), WidgetKit (rendering modes, accenting, visionOS textures/levelOfDetail/systemExtraLargePortrait) |
| references/migration-and-adoption.md | iOS 26 SDK migration: deadlines, two-phase strategy, grep audit, deprecated-API table, adoption lessons, back-compat adaptiveGlass(in:)/AdaptiveGlassButtonStyle, readiness checklist, UIKit gotchas (allowsLiquidTransform, floating-TabBar safe-area, UIDropShadowView, reversed bar items) |
| references/figma-to-glass.md | Figma → SwiftUI mapping, corner-radius/control-size tables, materials-vs-glass, color-scheme + accessibility behavior, GlassEffectContainer edge cases |
Apple documentation