Build guide · SwiftUI
Liquid glass in SwiftUI
On iOS 26 and later the system draws the glass: .glassEffect, GlassEffectContainer and the glass button styles bend the backdrop for you, and the system bars already are glass. What it does not decide for you is where glass belongs, how bodies group and morph, and what stays legible.
This is the SwiftUI recipe the Meniscus MCP server gives coding agents, with the iOS 27 additions at the end. Every block type-checks for iOS 26 and macOS 26.
Updated · the recipe the Meniscus MCP server returns for this medium
In short: .glassEffect, GlassEffectContainer and .buttonStyle(.glass); system bars already are glass.
Limits: .glassEffect, GlassEffectContainer and the glass button styles need iOS 26 / iPadOS 26 / macOS 26 (not visionOS); earlier versions get frost with no bend. The iOS 27 additions at the end need the 27 SDKs (Xcode 27). The system draws the bend, so the px bezel and displacement numbers are targets for judging screenshots, not API values.
Text("Continue").padding(.horizontal, 20).padding(.vertical, 12)
.glassEffect() // .regular in a capsule
Label("Share", systemImage: "square.and.arrow.up")
.padding()
.glassEffect(.regular.tint(.accentColor).interactive(), in: .rect(cornerRadius: 24, style: .continuous))
Button("Buy") { }.buttonStyle(.glassProminent) // tint material
Button("Later") { }.buttonStyle(.glass) // regular
/// Bodies in one container merge (morph) when closer than `spacing`; with `glassEffectID` a body that appears
/// grows out of its neighbour instead of fading in.
struct Tools: View {
@Namespace private var ns
@State private var expanded = false
var body: some View {
GlassEffectContainer(spacing: 16) {
HStack(spacing: 16) {
Button { withAnimation(.bouncy) { expanded.toggle() } } label: {
Image(systemName: "pencil").frame(width: 44, height: 44) // 44 pt target; a bare glyph is ~22 pt
}
.glassEffect(.regular.interactive())
.glassEffectID("pencil", in: ns)
if expanded {
Button { } label: { Image(systemName: "eraser").frame(width: 44, height: 44) }
.glassEffect(.regular.interactive())
.glassEffectID("eraser", in: ns)
}
}
}
}
}- Variants:
.regular(default, adaptive),.clear(clearest; needs a dim layer under it over bright media),.identity(off). Use.tint(_:)for prominence,.interactive()for press/drag response. - System bars, tab bars (
TabView), toolbars, sheets and menus adopt glass automatically; do not add another glass background behind them, and do not rebuild them from.glassEffect. Map the tab-bar, sheet and media-controls specs onto the system views (iOS 26;tabViewBottomAccessoryand.onScrollDownare unavailable on macOS):
TabView {
Tab("Library", systemImage: "square.stack") { LibraryView() }
Tab("Radio", systemImage: "dot.radiowaves.left.and.right") { RadioView() }
Tab(role: .search) { SearchView() } // the separate search lens
}
.tabBarMinimizeBehavior(.onScrollDown)
.tabViewBottomAccessory { MiniPlayer() } // mini player above the bar
MapView().sheet(isPresented: $showDetails) {
DetailsView().presentationDetents([.medium, .large]) // floating glass sheet
}- Before iOS 26, branch with
if #available(iOS 26, macOS 26, *); the fallback is.background(.ultraThinMaterial, in: .capsule)plus a 1 px gradient stroke overlay for the rim (frost, no bend).
iOS 27 / iPadOS 27 / macOS 27. The code blocks in this recipe are type-checked with Xcode 26; the iOS 27 names below come from Apple's iOS 27 documentation and have not been built here, so check their signatures in Xcode 27 and guard them with if #available(iOS 27, macOS 27, *).
- The system glass changed without any code change: stronger diffusion over busy content, a darker edge, brighter specular, and it follows the viewer's Settings › Appearance › Liquid Glass slider (ultra-clear to tinted). Custom
.glassEffectbodies follow the slider too; check screens at both ends, and give clear glass over bright media its dim layer (35 % black). - Built with the 27 SDKs,
UIDesignRequiresCompatibilityis ignored: every app gets the glass design. .scrollEdgeEffectStyle(.automatic)no longer switches between soft and hard and has its own look; do not force.hardto recreate the old top edge. The top bar gets one uniform band once content scrolls under it.- Tab bar: the prominent tab (
TabRole.prominent; the search tab when no tab is marked) sits apart at the trailing end and stays when the bar minimises. UIKit:UITabBarController.prominentTabIdentifier. - Toolbars:
toolbarMinimizationBehavior(_:for:)(.automatic,.never,.onScrollDown,.onScrollUp),ToolbarOverflowMenufor items that do not fit,.topBarPinnedTrailingfor the one item that must stay. - macOS 27: tighter window corners and edge-to-edge sidebars come from the system; use
NSView.cornerConfigurationwith.containerConcentricfor custom views near a window corner, andNSGlassEffectView.effectIsInteractivefor press response on AppKit glass. - Accessibility: read
accessibilityShowBorders(Show Borders) and give custom glass the increase-contrast border when it is on.
Liquid metal accent (not glass; one metal element per view): an angular-gradient rim on an opaque face.
let metal = AngularGradient(stops: [
.init(color: Color(white: 0.08), location: 0.00), .init(color: Color(white: 0.97), location: 0.10),
.init(color: Color(white: 0.55), location: 0.30), .init(color: .white, location: 0.45),
.init(color: Color(white: 0.17), location: 0.55), .init(color: Color(white: 0.86), location: 0.70),
.init(color: Color(white: 0.08), location: 0.81), .init(color: Color(white: 0.97), location: 0.92),
], center: .center, angle: .degrees(200))
Button { } label: { Label("Ask", systemImage: "sparkles").padding(.horizontal, 22).padding(.vertical, 13) }
.buttonStyle(.plain)
.foregroundStyle(Color(white: 0.96)) // the face is dark in both themes
.background(Capsule().fill(Color(white: 0.05)))
.overlay(Capsule().strokeBorder(metal, lineWidth: 3))- Motion: rotate
angleslowly (about 30° per second) inside aTimelineView(.animation); keep it still whenaccessibilityReduceMotionis on. - Flowing bands and red/blue fringes need the web
liquidMetalshader ported to a Metal[[ stitchable ]]function, applied with.colorEffect(ShaderLibrary.liquidMetal(...))to the rim or icon shape.
Border beam (one beamed element per view, border-beam effect spec): a trimmed copy of the outline redrawn every frame, so the beam keeps one speed and length on any shape. Type-checked for iOS 26 and macOS 26 and rendered with ImageRenderer.
/// A short arc of light travelling along `shape` at constant speed (trimmed path, not a rotating gradient).
struct BorderBeam<S: Shape>: View {
var shape: S
var lap: Double = 5 // seconds per lap: 4–6 buttons, 6–10 cards, 2–3 while working
var tail: CGFloat = 0.25 // fraction of the perimeter
var width: CGFloat = 1.5 // = rim width
var color: Color = .white // accent on light glass
var bloom: CGFloat = 4 // blur radius of the glow copy; 0 = none
var start: CGFloat = 0 // 0.5 for the second of twin beams
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
if !reduceMotion {
TimelineView(.animation) { context in
let t = CGFloat(context.date.timeIntervalSinceReferenceDate / lap)
let head = (t + start).truncatingRemainder(dividingBy: 1)
ZStack {
if bloom > 0 { beam(head: head, lineWidth: width * 2).blur(radius: bloom).opacity(0.8) }
beam(head: head, lineWidth: width)
}
}
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}
/// The tail as n nested strokes that all end at the head: stroke k starts k/n of the tail behind it, so
/// opacity builds up toward the head with no seams between pieces.
private func beam(head: CGFloat, lineWidth: CGFloat) -> some View {
Canvas { gc, size in
let path = shape.path(in: CGRect(origin: .zero, size: size).insetBy(dx: width / 2, dy: width / 2))
let n = 16
func target(_ k: Int) -> Double { k < 0 ? 0 : pow(Double(k + 1) / Double(n), 2) } // opacity in piece k
for k in 0..<n {
let alpha = 1 - (1 - target(k)) / (1 - target(k - 1))
for (from, to) in Self.wrapped(head - tail * CGFloat(n - k) / CGFloat(n), head) {
gc.stroke(path.trimmedPath(from: from, to: to), with: .color(color.opacity(alpha)), lineWidth: lineWidth)
}
}
}
}
/// Splits [a, b] (a may be below 0) into ranges inside 0...1.
private static func wrapped(_ a: CGFloat, _ b: CGFloat) -> [(CGFloat, CGFloat)] {
if b <= 0 { return [(a + 1, b + 1)] }
if a < 0 { return [(a + 1, 1), (0, b)] }
return [(a, b)]
}
}
Button("Get started") { }
.buttonStyle(.glassProminent)
.overlay { BorderBeam(shape: Capsule()) }
CardContent()
.glassEffect(.regular, in: .rect(cornerRadius: 28, style: .continuous))
.overlay {
ZStack { // twin beams
BorderBeam(shape: RoundedRectangle(cornerRadius: 28, style: .continuous), lap: 8)
BorderBeam(shape: RoundedRectangle(cornerRadius: 28, style: .continuous), lap: 8, start: 0.5)
}
}- The overlay shape must match the glass shape (
Capsule()for capsules,style: .continuousfor.rect). - Light glass:
color: .accentColor, width: 2. - Not
AngularGradient+rotationEffect: that is angle-driven and crawls mid-edge, then races round the caps.
<!-- medium: flutter -->
Questions
Which iOS version has liquid glass?
iOS 26, iPadOS 26 and macOS 26 introduced it; iOS 27 lowered the default transparency, darkened the rim and added a Clear to Tinted slider in Settings. Earlier versions get frost with no bend.
How do I make glass buttons morph into each other?
Put them in one GlassEffectContainer, give each a glassEffectID in a shared @Namespace, and change which ones exist inside withAnimation.
Should I stack glass on glass?
No. Glass belongs to the floating controls layer; a glass button inside a glass toolbar should be a plain button the toolbar's glass already covers.
The rules behind it
The code is half of it. Where glass belongs, which material each body gets, how it stays legible and what to reject are in the liquid glass guide and the 34 component specs. Other platforms: CSS and web, Flutter, React Native, Figma.