Apple's Liquid Glass design language across SwiftUI, AppKit, UIKit and WidgetKit, with the WWDC25 rules and code.
$ npx skills add https://craftwork.design/skills/liquid-glass -a claude-code -y
Installs into .claude/skills/liquid-glass. Add -g for every project.
SKILL.md
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 This Skill Activates
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
Liquid Glass is the material of the navigation layer — bars, toolbars, floating controls — never the content layer (tables, lists, rows in a scroll view).
Rule
Detail
Never glass on glass
Don't stack glass. Elements sitting ON glass don't get the material again — style them with fills and vibrancy.
Two variants — never mix them
Regular (default): works at any size, over anything, with adaptive legibility. Clear: only when ALL three hold — media-rich content underneath, a dimming layer is acceptable, and bold bright content sits above. Clear has no adaptive behaviors. One variant per interface.
Tint only primary actions
"When every element is tinted, nothing stands out."
No steady-state intersections
In resting layouts, content shouldn't sit half-under a glass element — reposition or scale the content instead.
Strip decorated bars
Remove customized bar backgrounds and borders; build hierarchy through layout and grouping, not decoration. Never group a symbol with a text label in one toolbar group. Action sheets spring from their source element.
Scroll edge effects keep floating elements separated from scrolling content: soft (gradual fade — the iOS default) vs hard (denser, with a dividing line — mostly macOS). One per view edge, and they're not decorative — don't add one where no floating UI elements exist.
Shape system — let containers do the math:
Shape
Radius
Use
Fixed
Constant
Standalone elements
Capsule
Half the element height
Phone-scale controls — add extra margin from the screen edge
Concentric
Parent radius minus padding
Nested containers (inner radii auto-calculate); iPad/Mac elements concentric with the window edge
Accessibility comes free at the system level: Reduced Motion decreases lensing and elastic effects; Increased Contrast renders glass elements black/white with a contrasting border. (Lensing is how glass appears — it materializes by modulating how it bends light, and adaptive shadows flip small elements light/dark for legibility over any content.)
When using multiple glass elements, wrap them in GlassEffectContainer for:
Better rendering performance
Proper blending between effects
Morphing transitions
This is correctness, not just performance: glass can not sample other glass, so nearby glass elements must share ONE container to render and blend correctly.
TabView {
// tabs
}
.tabBarMinimizeBehavior(.onScrollDown) // Tab bar recedes while scrolling content
.tabViewBottomAccessory { // Persistent control docked above the tab bar
MiniPlayerView()
}
Toolbar Spacers
Split toolbar items into separate glass groups instead of customizing bar backgrounds:
.toolbar {
ToolbarItem { editButton }
ToolbarSpacer(.fixed) // Visual break — starts a new glass group
ToolbarItem { shareButton }
ToolbarSpacer(.flexible) // Pushes the following group apart
ToolbarItem { doneButton }
}
Scroll Edge Effect Style
ScrollView { content }
.scrollEdgeEffectStyle(.hard, for: .top) // Hard style for dense UI, mostly macOS
Sheets
Remove custom presentationBackground modifiers — they interfere with the sheet's glass material and its morphing behavior.
For a card floating above content (e.g. overlaid on a map). Cards inside a scrolling list are content-layer — don't use glass there. Note the icon uses a fill, not a second glassEffect: elements on glass never get glass again.
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)
.background(.blue.opacity(0.15), in: .rect(cornerRadius: 12))
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)
}
}
}
}
}
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:
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()
}
}
}
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)
}