# Liquid glass in SwiftUI: glassEffect and iOS 27 changes

Source: https://meniscus.site/liquid-glass/swiftui · Updated 2026-10-01

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.

Ask your assistant (with Meniscus connected): "Build a SwiftUI photo viewer with a floating glass toolbar and a glass button group that morphs when tapped. Use the meniscus tools."

**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.
