Agent Skills: Liquid Glass Design

Implement Liquid Glass design using .glassEffect() API for iOS/macOS 26+. Use when creating modern glass-based UI effects.

UncategorizedID: rshankras/claude-code-apple-skills/liquid-glass

Install this agent skill to your local

pnpm dlx add-skill https://github.com/rshankras/claude-code-apple-skills/tree/HEAD/skills/design/liquid-glass

Skill Files

Browse the full folder contents for liquid-glass.

Download Skill

Loading file tree…

skills/design/liquid-glass/SKILL.md

Skill Metadata

Name
liquid-glass
Description
Implement Liquid Glass design using .glassEffect() API for iOS/macOS 26+. Covers SwiftUI, AppKit, UIKit, and WidgetKit. Use when creating modern glass-based UI effects.

Liquid Glass Design

Implement Apple's Liquid Glass design language across all Apple UI frameworks. Covers SwiftUI (.glassEffect()), AppKit (NSGlassEffectView), UIKit (UIGlassEffect + UIVisualEffectView), and WidgetKit (rendering modes, accented content, glass elements in widgets).

When to Use

  • User wants glass/blur effects on views
  • User asks about Liquid Glass or modern Apple design
  • User needs transparent, interactive UI elements
  • User wants morphing transitions between views
  • User is implementing glass effects in UIKit with UIVisualEffectView
  • User needs UIGlassEffect or UIGlassContainerEffect
  • User asks about scroll view edge effects in UIKit
  • User wants Liquid Glass in widgets (WidgetKit)
  • User needs to support accented rendering mode in widgets
  • User asks about widget textures or mounting styles on visionOS

Quick Start (SwiftUI)

Basic Glass Effect

import SwiftUI

Text("Hello, World!")
    .font(.title)
    .padding()
    .glassEffect()  // Capsule shape by default

Custom Shape

Text("Hello")
    .padding()
    .glassEffect(in: .rect(cornerRadius: 16))

// Available shapes:
// .capsule (default)
// .rect(cornerRadius: CGFloat)
// .circle

Interactive Glass

Button("Tap Me") {
    // action
}
.padding()
.glassEffect(.regular.interactive())

Tinted Glass

Text("Important")
    .padding()
    .glassEffect(.regular.tint(.blue))

Glass Configuration Options

| Option | Description | Example | |--------|-------------|---------| | .regular | Standard glass effect | .glassEffect(.regular) | | .tint(Color) | Add color tint | .glassEffect(.regular.tint(.orange)) | | .interactive() | React to touch/hover | .glassEffect(.regular.interactive()) |

Multiple Glass Effects

GlassEffectContainer

When using multiple glass elements, wrap them in GlassEffectContainer for:

  • Better rendering performance
  • Proper blending between effects
  • Morphing transitions
GlassEffectContainer(spacing: 40.0) {
    HStack(spacing: 40.0) {
        Image(systemName: "star.fill")
            .frame(width: 80, height: 80)
            .font(.system(size: 36))
            .glassEffect()

        Image(systemName: "heart.fill")
            .frame(width: 80, height: 80)
            .font(.system(size: 36))
            .glassEffect()
    }
}

Spacing Parameter:

  • Controls when effects merge
  • Smaller spacing = views must be closer to merge
  • Larger spacing = effects merge at greater distances

Uniting Glass Effects

Combine views into a single glass effect using glassEffectUnion:

@Namespace private var namespace

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

Morphing Transitions

Create fluid morphing effects when views appear/disappear.

Setup

  1. Create a namespace
  2. Assign glass effect IDs
  3. Use animations on state changes
struct MorphingToolbar: View {
    @State private var isExpanded = false
    @Namespace private var namespace

    var body: some View {
        GlassEffectContainer(spacing: 40.0) {
            HStack(spacing: 40.0) {
                // Always visible
                Image(systemName: "pencil")
                    .frame(width: 60, height: 60)
                    .glassEffect()
                    .glassEffectID("pencil", in: namespace)

                // Conditionally visible - will morph in/out
                if isExpanded {
                    Image(systemName: "eraser")
                        .frame(width: 60, height: 60)
                        .glassEffect()
                        .glassEffectID("eraser", in: namespace)

                    Image(systemName: "ruler")
                        .frame(width: 60, height: 60)
                        .glassEffect()
                        .glassEffectID("ruler", in: namespace)
                }
            }
        }

        Button("Toggle") {
            withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                isExpanded.toggle()
            }
        }
        .buttonStyle(.glass)
    }
}

Button Styles

Glass Button

Button("Standard") {
    // action
}
.buttonStyle(.glass)

Glass Prominent Button

Button("Primary Action") {
    // action
}
.buttonStyle(.glassProminent)

Advanced Techniques

Background Extension

Stretch content under sidebar or inspector:

NavigationSplitView {
    SidebarView()
} detail: {
    DetailView()
        .background {
            Image("wallpaper")
                .resizable()
                .ignoresSafeArea()
        }
}

Horizontal Scroll Under Sidebar

ScrollView(.horizontal) {
    HStack {
        ForEach(items) { item in
            ItemView(item: item)
        }
    }
}
.scrollExtensionMode(.underSidebar)

AppKit Implementation

NSGlassEffectView

import AppKit

// Create glass effect view
let glassView = NSGlassEffectView(frame: NSRect(x: 20, y: 20, width: 200, height: 100))
glassView.cornerRadius = 16.0
glassView.tintColor = NSColor.systemBlue.withAlphaComponent(0.3)

// Create content
let label = NSTextField(labelWithString: "Glass Content")
label.translatesAutoresizingMaskIntoConstraints = false

// Set content view
glassView.contentView = label

// Add constraints
if let contentView = glassView.contentView {
    NSLayoutConstraint.activate([
        label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
        label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
    ])
}

NSGlassEffectContainerView

// Create container
let container = NSGlassEffectContainerView(frame: bounds)
container.spacing = 40.0

// Create content view
let contentView = NSView(frame: container.bounds)
container.contentView = contentView

// Add glass views to content
let glass1 = NSGlassEffectView(frame: NSRect(x: 20, y: 50, width: 150, height: 100))
let glass2 = NSGlassEffectView(frame: NSRect(x: 190, y: 50, width: 150, height: 100))

contentView.addSubview(glass1)
contentView.addSubview(glass2)

Interactive AppKit Glass

class InteractiveGlassView: NSGlassEffectView {
    override init(frame: NSRect) {
        super.init(frame: frame)
        setupTracking()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupTracking()
    }

    private func setupTracking() {
        let options: NSTrackingArea.Options = [
            .mouseEnteredAndExited,
            .activeInActiveApp
        ]
        let trackingArea = NSTrackingArea(
            rect: bounds,
            options: options,
            owner: self,
            userInfo: nil
        )
        addTrackingArea(trackingArea)
    }

    override func mouseEntered(with event: NSEvent) {
        super.mouseEntered(with: event)
        NSAnimationContext.runAnimationGroup { context in
            context.duration = 0.2
            animator().tintColor = NSColor.systemBlue.withAlphaComponent(0.2)
        }
    }

    override func mouseExited(with event: NSEvent) {
        super.mouseExited(with: event)
        NSAnimationContext.runAnimationGroup { context in
            context.duration = 0.2
            animator().tintColor = nil
        }
    }
}

Common Patterns

Floating Action Bar

struct FloatingActionBar: View {
    @Namespace private var namespace

    var body: some View {
        GlassEffectContainer(spacing: 20) {
            HStack(spacing: 16) {
                ForEach(actions) { action in
                    Button {
                        action.perform()
                    } label: {
                        Image(systemName: action.icon)
                            .font(.title2)
                    }
                    .frame(width: 44, height: 44)
                    .glassEffect(.regular.interactive())
                    .glassEffectID(action.id, in: namespace)
                }
            }
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
        }
    }
}

Card with Glass Effect

struct GlassCard: View {
    let title: String
    let subtitle: String
    let icon: String

    var body: some View {
        HStack(spacing: 16) {
            Image(systemName: icon)
                .font(.title)
                .frame(width: 50, height: 50)
                .glassEffect(.regular.tint(.blue))

            VStack(alignment: .leading) {
                Text(title)
                    .font(.headline)
                Text(subtitle)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            Spacer()
        }
        .padding()
        .glassEffect(in: .rect(cornerRadius: 16))
    }
}

Tab Bar with Morphing

struct GlassTabBar: View {
    @Binding var selection: Int
    @Namespace private var namespace

    let tabs = [
        ("house", "Home"),
        ("magnifyingglass", "Search"),
        ("person", "Profile")
    ]

    var body: some View {
        GlassEffectContainer(spacing: 30) {
            HStack(spacing: 30) {
                ForEach(tabs.indices, id: \.self) { index in
                    Button {
                        withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
                            selection = index
                        }
                    } label: {
                        VStack(spacing: 4) {
                            Image(systemName: tabs[index].0)
                                .font(.title2)
                            Text(tabs[index].1)
                                .font(.caption)
                        }
                        .frame(width: 70, height: 60)
                    }
                    .glassEffect(
                        selection == index
                            ? .regular.tint(.blue).interactive()
                            : .regular.interactive()
                    )
                    .glassEffectID("tab\(index)", in: namespace)
                }
            }
        }
    }
}

Migration from Old API

Before (Old Approach)

// Old: Using materials directly
VStack {
    Text("Content")
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(16)

After (New API)

// New: Using glassEffect modifier
VStack {
    Text("Content")
}
.padding()
.glassEffect(in: .rect(cornerRadius: 16))

Key Differences

| Old Approach | New API | |--------------|---------| | .background(.material) | .glassEffect() | | Manual corner radius | Shape parameter | | No interactivity | .interactive() modifier | | Manual tinting | .tint(Color) modifier | | No morphing | glassEffectID + @Namespace | | No container grouping | GlassEffectContainer |

UIKit Implementation

UIGlassEffect

Use UIVisualEffectView with a UIGlassEffect to create glass surfaces in UIKit:

import UIKit

let glassEffect = UIGlassEffect()
let visualEffectView = UIVisualEffectView(effect: glassEffect)
visualEffectView.frame = CGRect(x: 50, y: 100, width: 300, height: 200)
visualEffectView.layer.cornerRadius = 20
visualEffectView.clipsToBounds = true

let label = UILabel()
label.text = "Liquid Glass"
label.textAlignment = .center
label.frame = visualEffectView.bounds
visualEffectView.contentView.addSubview(label)
view.addSubview(visualEffectView)

Customizing the Glass Effect

glassEffect.tintColor = UIColor.systemBlue.withAlphaComponent(0.3)
glassEffect.isInteractive = true

Interactive Glass in UIKit

Set isInteractive = true on a UIGlassEffect to make it respond to touch:

let interactiveGlassEffect = UIGlassEffect()
interactiveGlassEffect.isInteractive = true

let glassButton = UIButton(frame: CGRect(x: 50, y: 300, width: 200, height: 50))
glassButton.setTitle("Glass Button", for: .normal)
glassButton.setTitleColor(.white, for: .normal)

let buttonEffectView = UIVisualEffectView(effect: interactiveGlassEffect)
buttonEffectView.frame = glassButton.bounds
buttonEffectView.layer.cornerRadius = 15
buttonEffectView.clipsToBounds = true

glassButton.insertSubview(buttonEffectView, at: 0)
view.addSubview(glassButton)

UIGlassContainerEffect

Use UIGlassContainerEffect when combining multiple glass elements. This is the UIKit equivalent of SwiftUI's GlassEffectContainer -- it enables proper blending and morphing between glass views:

let containerEffect = UIGlassContainerEffect()
containerEffect.spacing = 40.0

let containerView = UIVisualEffectView(effect: containerEffect)
containerView.frame = CGRect(x: 50, y: 400, width: 300, height: 200)

let firstGlassEffect = UIGlassEffect()
let firstGlassView = UIVisualEffectView(effect: firstGlassEffect)
firstGlassView.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
firstGlassView.layer.cornerRadius = 20
firstGlassView.clipsToBounds = true

let secondGlassEffect = UIGlassEffect()
secondGlassEffect.tintColor = UIColor.systemPink.withAlphaComponent(0.3)
let secondGlassView = UIVisualEffectView(effect: secondGlassEffect)
secondGlassView.frame = CGRect(x: 80, y: 60, width: 100, height: 100)
secondGlassView.layer.cornerRadius = 20
secondGlassView.clipsToBounds = true

containerView.contentView.addSubview(firstGlassView)
containerView.contentView.addSubview(secondGlassView)
view.addSubview(containerView)

Scroll View Edge Effects

UIKit scroll views now support configurable edge effects for Liquid Glass integration:

let scrollView = UIScrollView(frame: view.bounds)
scrollView.topEdgeEffect.style = .automatic
scrollView.bottomEdgeEffect.style = .hard
scrollView.leftEdgeEffect.isHidden = true
scrollView.rightEdgeEffect.isHidden = true

Available Edge Effect Styles:

| Style | Description | |-------|-------------| | .automatic | System determines style based on context | | .hard | Hard cutoff with a dividing line |

UIScrollEdgeElementContainerInteraction

Use UIScrollEdgeElementContainerInteraction to coordinate glass elements (such as bottom toolbars) with scroll edge behavior:

let interaction = UIScrollEdgeElementContainerInteraction()
interaction.scrollView = scrollView
interaction.edge = .bottom
buttonContainer.addInteraction(interaction)

Toolbar Integration

UIKit navigation bar items integrate with Liquid Glass automatically. Use hidesSharedBackground to opt individual items out of the shared glass bar:

let shareButton = UIBarButtonItem(
    barButtonSystemItem: .action,
    target: self,
    action: #selector(shareAction)
)
let favoriteButton = UIBarButtonItem(
    image: UIImage(systemName: "heart"),
    style: .plain,
    target: self,
    action: #selector(favoriteAction)
)
favoriteButton.hidesSharedBackground = true
navigationItem.rightBarButtonItems = [shareButton, favoriteButton]

UIKit vs SwiftUI Comparison

| SwiftUI | UIKit | |---------|-------| | .glassEffect() | UIVisualEffectView(effect: UIGlassEffect()) | | .glassEffect(.regular.interactive()) | UIGlassEffect() with isInteractive = true | | .glassEffect(.regular.tint(.blue)) | UIGlassEffect() with tintColor = ... | | GlassEffectContainer(spacing:) | UIGlassContainerEffect() with spacing | | .buttonStyle(.glass) | Insert UIVisualEffectView as button subview |

WidgetKit Implementation

Rendering Modes

Widgets support two rendering modes that affect how Liquid Glass is displayed:

| Mode | Description | |------|-------------| | Full Color | Default mode. Displays all colors, images, and transparency as designed. | | Accented | Used when tinted or clear appearance is chosen. Primary and accented content tinted white (iOS and macOS). Background replaced with themed glass or tinted color effect. |

Accented Mode

Detect the rendering mode and adapt layout accordingly. Use .widgetAccentable() to mark views that should be tinted in accented mode:

struct MyWidgetView: View {
    @Environment(\.widgetRenderingMode) var renderingMode

    var body: some View {
        if renderingMode == .accented {
            // Layout optimized for accented mode
            AccentedWidgetLayout()
        } else {
            // Standard full-color layout
            FullColorWidgetLayout()
        }
    }
}

Grouping Accent Content

HStack(alignment: .center, spacing: 0) {
    VStack(alignment: .leading) {
        Text("Widget Title")
            .font(.headline)
            .widgetAccentable()
        Text("Widget Subtitle")
    }
    Image(systemName: "star.fill")
        .widgetAccentable()
}

Image Rendering in Accented Mode

Image("myImage")
    .widgetAccentedRenderingMode(.monochrome)

Container Backgrounds

Define a container background for your widget content:

var body: some View {
    VStack {
        // Widget content
    }
    .containerBackground(for: .widget) {
        Color.blue.opacity(0.2)
    }
}

Background Removal

Prevent the system from removing the widget background. Note that marking a background as non-removable excludes the widget from contexts that require removable backgrounds (iPad Lock Screen, StandBy):

var body: some WidgetConfiguration {
    StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
        MyWidgetView(entry: entry)
    }
    .containerBackgroundRemovable(false)
}

visionOS Textures and Mounting Styles

Widget Textures

// Default glass texture
.widgetTexture(.glass)

// Paper-like texture
.widgetTexture(.paper)

Mounting Styles

.supportedMountingStyles([.recessed, .elevated])

| Style | Description | |-------|-------------| | .recessed | Widget appears embedded into a vertical surface | | .elevated | Widget appears on top of a surface |

Custom Glass Elements in Widgets

Apply .glassEffect() and .buttonStyle(.glass) directly within widget views:

// Glass text element
Text("Custom Element")
    .padding()
    .glassEffect()

// Glass image element
Image(systemName: "star.fill")
    .frame(width: 60, height: 60)
    .glassEffect(.regular, in: .rect(cornerRadius: 12))

// Glass button in widget
Button("Action") { }
    .buttonStyle(.glass)

Best Practices

  1. Use GlassEffectContainer for multiple glass views

    • Improves rendering performance
    • Enables morphing transitions
  2. Apply glass effect last in modifier chain

    • After frame, padding, and content modifiers
  3. Choose appropriate spacing in containers

    • Controls when effects blend together
  4. Use animations for state changes

    • Enables smooth morphing transitions
  5. Add interactivity for touchable elements

    • .interactive() for buttons and controls
  6. Tint strategically to indicate state

    • Selected items, primary actions
  7. Consistent shapes across your app

    • Establish a shape language (all capsules, or all rounded rects)

Checklist

SwiftUI

  • [ ] Use .glassEffect() instead of .background(.material)
  • [ ] Wrap multiple glass views in GlassEffectContainer
  • [ ] Add @Namespace for morphing transitions
  • [ ] Use .glassEffectID() on views that appear/disappear
  • [ ] Add .interactive() for touchable elements
  • [ ] Use .buttonStyle(.glass) for glass buttons
  • [ ] Test animations for smooth morphing

UIKit

  • [ ] Use UIVisualEffectView with UIGlassEffect for glass surfaces
  • [ ] Set isInteractive = true on glass effects for touchable elements
  • [ ] Wrap multiple glass views in UIGlassContainerEffect
  • [ ] Configure scroll view edge effects (.automatic or .hard)
  • [ ] Use UIScrollEdgeElementContainerInteraction for scroll-coordinated toolbars
  • [ ] Use hidesSharedBackground for toolbar items that need independent glass

WidgetKit

  • [ ] Detect widgetRenderingMode and adapt layout for accented mode
  • [ ] Mark accent content with .widgetAccentable()
  • [ ] Set .widgetAccentedRenderingMode() on images
  • [ ] Define .containerBackground(for: .widget) for backgrounds
  • [ ] Use .containerBackgroundRemovable(false) only when necessary
  • [ ] Apply .glassEffect() and .buttonStyle(.glass) in widget views
  • [ ] Configure .widgetTexture() and .supportedMountingStyles() for visionOS

General

  • [ ] Consider performance with many glass effects
  • [ ] Support both light and dark appearances

References

SwiftUI

AppKit

UIKit

WidgetKit