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.

swift
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; tabViewBottomAccessory and .onScrollDown are unavailable on macOS):
swift
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 .glassEffect bodies 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, UIDesignRequiresCompatibility is ignored: every app gets the glass design.
  • .scrollEdgeEffectStyle(.automatic) no longer switches between soft and hard and has its own look; do not force .hard to 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), ToolbarOverflowMenu for items that do not fit, .topBarPinnedTrailing for the one item that must stay.
  • macOS 27: tighter window corners and edge-to-edge sidebars come from the system; use NSView.cornerConfiguration with .containerConcentric for custom views near a window corner, and NSGlassEffectView.effectIsInteractive for 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.

swift
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 angle slowly (about 30° per second) inside a TimelineView(.animation); keep it still when accessibilityReduceMotion is on.
  • Flowing bands and red/blue fringes need the web liquidMetal shader 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.

swift
/// 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: .continuous for .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.