swiftui-liquid-glass

$npx mdskill add dpearson2699/swift-ios-skills/swiftui-liquid-glass

Implement, review, or improve SwiftUI Liquid Glass effects for iOS 26+.

  • Helps adopt dynamic translucent glass on controls and navigation surfaces.
  • Uses glassEffect modifier, GlassEffectContainer, and glass button styles.
  • Decides between static badges and interactive controls based on context.
  • Provides code examples, review checklist, and availability gating guidance.

SKILL.md

.github/skills/swiftui-liquid-glassView on GitHub ↗
---
name: swiftui-liquid-glass
description: "Implement, review, or improve SwiftUI Liquid Glass effects for iOS 26+. Covers glassEffect modifier, GlassEffectContainer, glass button styles, glass toolbar/tab bar, static status badges vs interactive controls, morphing transitions, tinting, interactive glass, ToolbarSpacer, scrollEdgeEffectStyle, backgroundExtensionEffect, and availability gating. Use when asked about Liquid Glass, glass buttons, glassEffect, GlassEffectTransition, glassEffectID, glassEffectUnion, scroll edge effects, or adopting iOS 26 design."
---

# SwiftUI Liquid Glass

Liquid Glass is the dynamic translucent material introduced across Apple platforms
26. Standard SwiftUI bars and presentations adopt it automatically when built with
the current SDK. Reserve custom glass for functional controls and navigation surfaces,
not general content backgrounds.

See [references/liquid-glass.md](references/liquid-glass.md) for the full API reference with additional examples.

## Contents

- [Workflow](#workflow)
- [Core API Summary](#core-api-summary)
- [Code Examples](#code-examples)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Workflow

Choose the path that matches the request:

### 1. Implement a new feature with Liquid Glass

1. Identify target surfaces (floating controls, custom bars, transient controls).
2. Decide shape, prominence, and whether each element is a real control or static status.
3. Wrap grouped glass elements in a `GlassEffectContainer`.
4. Apply `.glassEffect()` **after** layout and appearance modifiers.
5. Add `.interactive()` only to tappable/focusable elements.
6. Add morphing transitions with `glassEffectID(_:in:)` where the view hierarchy
   changes with animation. Put `glassEffectTransition(_:)` on the inserted or
   removed glass child, not on the always-present container, and choose the style
   using [Morphing & Transitions](#morphing--transitions).
7. Gate with `if #available(iOS 26, *)` and provide a fallback for earlier versions.

### 2. Improve an existing feature with Liquid Glass

1. Find custom control or navigation backgrounds that can be replaced with `.glassEffect()`.
2. Wrap sibling glass elements in `GlassEffectContainer` for blending and performance.
3. Replace custom glass-like buttons with `.buttonStyle(.glass)`, `.buttonStyle(.glassProminent)`, or configurable styles such as `.buttonStyle(.glass(.clear))`. Use `.glass(_:)` when the button needs a specific tint or variant; reserve `.glassProminent` for high-emphasis primary actions.
4. Add morphing transitions where animated insertion/removal occurs.

### 3. Review existing Liquid Glass usage

Trace each effect through content/control ownership, layout order, container scope, interactivity, transitions, availability, accessibility settings, and fallback. Restore the same UI fixture and rerun the checklist after each correction.

## Core API Summary

### glassEffect(_:in:)

Applies Liquid Glass behind a view. Default: `.regular` variant in a `Capsule` shape.

```swift
nonisolated func glassEffect(
    _ glass: Glass = .regular,
    in shape: some Shape = DefaultGlassEffectShape()
) -> some View
```

### Glass struct

| Property / Method | Purpose |
|---|---|
| `.regular` | Standard glass material |
| `.clear` | Clear variant; add dimming/contrast treatment when legibility needs it |
| `.identity` | No visual effect (pass-through) |
| `.tint(_:)` | Add a color tint for prominence |
| `.interactive(_:)` | React to touch and pointer interactions |

Chain them: `.regular.tint(.blue).interactive()`

### GlassEffectContainer

Wraps multiple glass views for shared rendering, blending, and morphing.

```swift
GlassEffectContainer(spacing: 24) {
    // child views with .glassEffect()
}
```

The `spacing` controls when nearby glass shapes begin to blend. Match or exceed
the interior layout spacing so shapes merge during animated transitions but remain
separate at rest.

### Morphing & Transitions

| Modifier | Purpose |
|---|---|
| `glassEffectID(_:in:)` | Stable identity for morphing during view hierarchy changes |
| `glassEffectUnion(id:namespace:)` | Merge multiple views into one glass shape |
| `glassEffectTransition(_:)` | Control how glass appears/disappears |

Transition decision: use `.matchedGeometry` for nearby effects inside the container's
spacing; use `.materialize` for distant insertion/removal or when no geometry match
should occur; use `.identity` only when no transition animation is wanted.

### Button Styles

```swift
Button("Action") { }
    .buttonStyle(.glass)           // standard glass button

Button("Primary") { }
    .buttonStyle(.glassProminent)  // prominent glass button

Button("Media") { }
    .buttonStyle(.glass(.clear))    // configurable variant; verify contrast
```

### Related iOS 26 APIs

| API | Use |
|---|---|
| `scrollEdgeEffectStyle` | Configure a scroll boundary's visual treatment. |
| `backgroundExtensionEffect` | Extend one background under safe-area edges with mirrored blur. |
| `ToolbarSpacer` | Create a visual break between toolbar items. |

See the corresponding sections in
[references/liquid-glass.md](references/liquid-glass.md) for signatures and examples.

## Code Examples

### Glass button with availability gate

```swift
if #available(iOS 26, *) {
    Button("Show Status") { showStatusDetails() }
        .buttonStyle(.glass)
} else {
    Button("Show Status") { showStatusDetails() }
        .buttonStyle(.bordered)
}
```

### Grouped glass shapes in a container

```swift
let symbols = ["pencil", "eraser.fill", "lasso"]

GlassEffectContainer(spacing: 24) {
    HStack(spacing: 24) {
        ForEach(symbols, id: \.self) { symbol in
            Image(systemName: symbol)
                .frame(width: 56, height: 56)
                .glassEffect()
        }
    }
}
```

### Nearby morphing transition

```swift
@State private var isExpanded = false
@Namespace private var ns

var body: some View {
    GlassEffectContainer(spacing: 40) {
        HStack(spacing: 40) {
            Image(systemName: "pencil")
                .frame(width: 80, height: 80)
                .glassEffect()
                .glassEffectID("pencil", in: ns)

            if isExpanded {
                Image(systemName: "eraser.fill")
                    .frame(width: 80, height: 80)
                    .glassEffect()
                    .glassEffectID("eraser", in: ns)
                    .glassEffectTransition(.matchedGeometry)
            }
        }
    }

    Button("Toggle") {
        withAnimation { isExpanded.toggle() }
    }
    .buttonStyle(.glass)
}
```

### Unioning views into a single glass shape

```swift
@Namespace private var ns

GlassEffectContainer(spacing: 20) {
    HStack(spacing: 20) {
        ForEach(items.indices, id: \.self) { i in
            Image(systemName: items[i])
                .frame(width: 80, height: 80)
                .glassEffect()
                .glassEffectUnion(id: i < 2 ? "group1" : "group2", namespace: ns)
        }
    }
}
```

### Tinted glass icon control

```swift
struct GlassIconControl: View {
    let icon: String
    let tint: Color
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Image(systemName: icon)
                .font(.title2)
                .padding()
        }
        .buttonStyle(.glass(.regular.tint(tint)))
    }
}
```

### Clear glass action over bright content

```swift
if #available(iOS 26, *) {
    ZStack {
        Capsule()
            .fill(.black.opacity(0.28))

        Button {
            playRecap()
        } label: {
            Label("Play", systemImage: "play.fill")
                .font(.headline)
                .padding(.horizontal, 8)
        }
        .buttonStyle(.glass(.clear))
    }
    .fixedSize()
} else {
    Button("Play", systemImage: "play.fill") { playRecap() }
        .buttonStyle(.borderedProminent)
}
```

Use clear glass only when the background still leaves labels and symbols readable.
On bright or busy backgrounds, add a subtle dimming layer or choose a more opaque
button style.

### Static status count, not a toolbar control

```swift
VStack(spacing: 8) {
    Text("24")
        .font(.headline.monospacedDigit())
        .accessibilityLabel("24 options expiring")

    Text("Options Expiring")
        .font(.subheadline)
        .foregroundStyle(.secondary)
}
// Keep read-only status out of toolbar control slots and do not add .interactive().
```

## Common Mistakes

| Mistake | Fix |
|---|---|
| Glass decorates static content | Keep it in the controls/navigation layer. |
| Read-only status uses `.interactive()` or an action slot | Present status as content, or make the whole badge one real accessible action. |
| Related effects use nested containers | Use one `GlassEffectContainer` per related blending/morphing group. |
| `.glassEffect()` precedes padding/frame/style | Apply layout and appearance first, then glass. |
| Custom effects assume default accessibility settings | Test Reduce Transparency, Reduce Motion, contrast, and legibility. |
| No pre-iOS 26 path | Gate the effect and preserve a functional fallback. |

## Review Checklist

- [ ] **Availability**: `if #available(iOS 26, *)` present with fallback UI.
- [ ] **Container**: Multiple glass views wrapped in `GlassEffectContainer`.
- [ ] **Modifier order**: `.glassEffect()` applied after layout/appearance modifiers.
- [ ] **Interactivity**: `.interactive()` used only where user interaction exists.
- [ ] **Status vs action**: Static counts/status are not toolbar controls and do not expose press/hover affordance.
- [ ] **Transitions**: `glassEffectID` used with `@Namespace` for morphing animations.
- [ ] **Transition type**: `.matchedGeometry` for nearby effects; `.materialize` for distant ones, applied to the conditional glass child.
- [ ] **Consistency**: Shapes, tints, and spacing are uniform across related elements.
- [ ] **Performance**: Glass effects are limited in number; container used for grouping.
- [ ] **Accessibility**: Tested with Reduce Transparency and Reduce Motion enabled.
- [ ] **Button styles**: Standard `.glass`, `.glassProminent`, or configurable `.glass(_:)` used for buttons; `.glass(_:)` is primary when a tint or clear variant is required.
- [ ] **Clear glass contrast**: Clear glass over bright content has a dimming/contrast treatment or uses a more legible style.
- [ ] **Concurrency**: IDs passed to `glassEffectID` / `glassEffectUnion` are `Sendable`; MainActor-annotated Liquid Glass APIs stay in SwiftUI UI code.

## References

- Full API guide: [references/liquid-glass.md](references/liquid-glass.md)
- Apple docs: [Applying Liquid Glass to custom views](https://sosumi.ai/documentation/swiftui/Applying-Liquid-Glass-to-custom-views)
- Apple docs: [Adopting Liquid Glass](https://sosumi.ai/documentation/technologyoverviews/adopting-liquid-glass)

More from dpearson2699/swift-ios-skills

SkillDescription
accessorysetupkitDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
activitykitImplement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
adattributionkitMeasure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
alarmkitImplement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, system Stop and AlarmButton secondary actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
app-clipsBuild iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
app-intentsImplement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
app-store-reviewAudits App Store submission readiness and rejection risk across current review guidelines, PrivacyInfo.xcprivacy and required-reason APIs, privacy labels, ATT, StoreKit payments, metadata, entitlements, widgets, and Live Activities. Use when preparing a submission, responding to rejection, reconciling privacy evidence, or separating upload blockers from cleanup.
apple-on-device-aiBuild private, on-device AI features on iPhone, iPad, and Mac with Foundation Models, Core ML, MLX Swift, or llama.cpp. Use when choosing an Apple-local model runtime, building an Apple Intelligence chatbot or tool-calling feature, running an LLM on Apple Silicon, converting or compressing a Python model for Core ML, or comparing on-device inference backends. For Swift Core ML loading and prediction code, use the coreml skill.
appmigrationkitTransfer app data to or from other platforms using AppMigrationKit. Use when implementing system-orchestrated one-time migration between iOS and Android or another platform, building an AppMigrationExtension, packaging transportable resources with ResourcesArchiver, importing resources on the destination device, reporting import progress, handling migration errors and app group cleanup, checking MigrationStatus, or testing migration code with AppMigrationTester.
audioaccessorykitSupport automatic audio switching for paired third-party Bluetooth headphones or earbuds with AudioAccessoryKit. Use when a companion app registers an audio accessory, an app extension reports worn/removed placement or connected source-device changes, or AccessoryControlDevice capabilities and errors need handling. Do not use for general AVAudioSession routing, Bluetooth transport, or initial accessory pairing.