skills/liquid-glass/SKILL.md
Apple Liquid Glass design patterns for SwiftUI iOS 26 - glass effects, morphing, containers, interactive glass, tinting, accessibility, and cross-platform glass design.
npx skillsauth add ahmed3elshaer/everything-claude-code-mobile liquid-glassInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Comprehensive guide to implementing Liquid Glass design patterns introduced at WWDC 2025 for iOS 26+.
Minimum Deployment Target: iOS 26.0+ / macOS 26.0+ / watchOS 26.0+ / tvOS 26.0+ / visionOS 26.0+
Liquid Glass is the defining visual language of iOS 26, introduced at WWDC 2025. It is a translucent material system that reflects, refracts, and dynamically responds to the content beneath it. Glass lives exclusively in the navigation layer -- the floating controls, toolbars, tab bars, and navigation bars that sit above your content.
.glassEffect()The primary modifier for applying Liquid Glass to any SwiftUI view.
func glassEffect<S: Shape>(
_ glass: Glass = .regular,
in shape: S = DefaultGlassEffectShape,
isEnabled: Bool = true
) -> some View
| Variant | Transparency | Use Case |
|---------|-------------|----------|
| .regular | Medium | Toolbars, buttons, nav bars, tab bars -- the default for most controls |
| .clear | High | Floating controls over media-rich backgrounds (photos, maps, video) |
| .identity | None (pass-through) | Conditional disabling of the glass effect |
// ✅ Default glass effect
Button("Settings") {
showSettings()
}
.glassEffect()
// ✅ Clear glass for media overlays
Button(action: { togglePlay() }) {
Image(systemName: "play.fill")
.font(.title2)
}
.glassEffect(.clear)
// ✅ Conditional glass
Button("Action") { performAction() }
.glassEffect(.regular, isEnabled: showGlass)
// ✅ Disabled glass (identity)
Text("No Glass")
.glassEffect(.identity)
// ✅ Glass with rounded rectangle
Button("Save") { save() }
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 16))
// ✅ Glass in a circle
Button(action: {}) {
Image(systemName: "plus")
}
.glassEffect(.regular, in: .circle)
// ✅ Glass in a capsule (default shape)
Button("Next") { next() }
.glassEffect(.regular, in: .capsule)
Tinting adds a subtle color wash to the glass material. Use it to convey semantic meaning, brand identity, or visual grouping.
.glassEffect(.regular.tint(.blue))
.glassEffect(.regular.tint(.purple.opacity(0.6)))
.glassEffect(.clear.tint(.green))
// ✅ Semantic tinting for a destructive action
Button("Delete", role: .destructive) {
deleteItem()
}
.glassEffect(.regular.tint(.red))
// ✅ Brand color tinting
Button("Subscribe") {
subscribe()
}
.glassEffect(.regular.tint(Color.accentColor.opacity(0.5)))
// ✅ Subtle tint for grouping related controls
HStack(spacing: 12) {
Button("Bold") { toggleBold() }
.glassEffect(.regular.tint(.blue.opacity(0.3)))
Button("Italic") { toggleItalic() }
.glassEffect(.regular.tint(.blue.opacity(0.3)))
Button("Underline") { toggleUnderline() }
.glassEffect(.regular.tint(.blue.opacity(0.3)))
}
Interactive glass adds touch-responsive physical behaviors to glass elements. This is an iOS-only feature and has no effect on macOS, watchOS, tvOS, or visionOS.
.glassEffect(.regular.interactive())
| Behavior | Description | |----------|-------------| | Scale on press | Glass element subtly scales down when pressed | | Bounce animation | Spring-based bounce when released | | Shimmering | Light shimmer effect during interaction | | Touch-point illumination | Localized glow at the exact point of touch | | Gesture response | Glass reacts to drag, long press, and tap gestures |
// ✅ Interactive floating action button
Button(action: { createNewItem() }) {
Image(systemName: "plus")
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(.white)
.padding(16)
}
.glassEffect(.regular.interactive())
// ✅ Interactive with tinting
Button("Add to Cart") {
addToCart()
}
.padding(.horizontal, 24)
.padding(.vertical, 12)
.glassEffect(.regular.interactive().tint(.blue))
// ✅ Interactive clear glass over photo
Button(action: { toggleFavorite() }) {
Image(systemName: isFavorite ? "heart.fill" : "heart")
.foregroundStyle(isFavorite ? .red : .white)
}
.glassEffect(.clear.interactive())
.interactive() to non-interactive views (labels, decorations).tint() for colored interactive glassGlass can be rendered in any SwiftUI Shape. The shape determines the outline, corner radii, and how concentric inner shapes are computed.
// Capsule (default) -- radius equals 50% of height
.glassEffect(.regular, in: .capsule)
// Circle
.glassEffect(.regular, in: .circle)
// Rounded rectangle with explicit radius
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 16))
// Concentric rounded rectangle (radius derived from parent)
.glassEffect(.regular, in: .rect(cornerRadius: .containerConcentric))
// Ellipse
.glassEffect(.regular, in: .ellipse)
// ✅ Custom shape conforming to Shape protocol
struct DiamondShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: center.x, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: center.y))
path.addLine(to: CGPoint(x: center.x, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: center.y))
path.closeSubpath()
return path
}
}
Button(action: {}) {
Image(systemName: "star.fill")
}
.glassEffect(.regular, in: DiamondShape())
Fixed Shapes -- Constant corner radius regardless of context. Use RoundedRectangle(cornerRadius: 16) for explicit control.
Capsules -- Radius is always 50% of the shorter dimension. Capsules automatically produce concentric inner shapes when nesting glass.
Concentric Shapes -- Radius equals parent radius - padding. Use .rect(cornerRadius: .containerConcentric) for inner elements that should match their container's curvature.
// ✅ Concentric nesting example
VStack {
Button("Inner Action") {}
.glassEffect(.regular, in: .rect(cornerRadius: .containerConcentric))
}
.padding(8)
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 24))
GlassEffectContainer is the grouping primitive for glass elements. It serves two critical functions:
GlassEffectContainer(spacing: CGFloat? = nil) {
// Content with .glassEffect() modifiers
}
// ✅ Toolbar with multiple glass buttons
GlassEffectContainer(spacing: 12) {
Button(action: { undo() }) {
Image(systemName: "arrow.uturn.backward")
}
.glassEffect()
Button(action: { redo() }) {
Image(systemName: "arrow.uturn.forward")
}
.glassEffect()
Button(action: { share() }) {
Image(systemName: "square.and.arrow.up")
}
.glassEffect()
}
// ✅ CORRECT: Multiple glass elements grouped in a container
GlassEffectContainer(spacing: 16) {
Button("Cancel") { cancel() }
.glassEffect()
Button("Save") { save() }
.glassEffect(.regular.tint(.blue))
}
// ❌ WRONG: Multiple glass elements without a container
HStack(spacing: 16) {
Button("Cancel") { cancel() }
.glassEffect()
Button("Save") { save() }
.glassEffect(.regular.tint(.blue))
}
// ✅ Glass container with vertical layout
GlassEffectContainer(spacing: 8) {
VStack(spacing: 8) {
Button("Option A") { selectA() }
.glassEffect()
Button("Option B") { selectB() }
.glassEffect()
Button("Option C") { selectC() }
.glassEffect()
}
}
// ✅ Nested containers for complex layouts
GlassEffectContainer(spacing: 16) {
HStack(spacing: 12) {
GlassEffectContainer(spacing: 4) {
Button(action: {}) { Image(systemName: "bold") }
.glassEffect()
Button(action: {}) { Image(systemName: "italic") }
.glassEffect()
}
GlassEffectContainer(spacing: 4) {
Button(action: {}) { Image(systemName: "list.bullet") }
.glassEffect()
Button(action: {}) { Image(systemName: "list.number") }
.glassEffect()
}
}
}
GlassEffectContainerGlass morphing creates fluid, animated transitions between glass shapes when views appear, disappear, or change layout. This is one of the most visually striking features of Liquid Glass.
GlassEffectContainerglassEffectID with a shared NamespacewithAnimation(.bouncy))func glassEffectID<ID: Hashable>(_ id: ID, in namespace: Namespace.ID) -> some View
struct MorphingToolbar: View {
@Namespace private var namespace
@State private var isExpanded = false
var body: some View {
GlassEffectContainer(spacing: 12) {
Button(action: {
withAnimation(.bouncy) {
isExpanded.toggle()
}
}) {
Image(systemName: isExpanded ? "chevron.left" : "chevron.right")
}
.glassEffect()
.glassEffectID("toggle", in: namespace)
if isExpanded {
Button("Cut") { cut() }
.glassEffect()
.glassEffectID("cut", in: namespace)
Button("Copy") { copy() }
.glassEffect()
.glassEffectID("copy", in: namespace)
Button("Paste") { paste() }
.glassEffect()
.glassEffectID("paste", in: namespace)
}
}
}
}
struct GlassTabBar: View {
@Namespace private var namespace
@State private var selectedTab = 0
let tabs = ["Home", "Search", "Profile"]
var body: some View {
GlassEffectContainer(spacing: 8) {
HStack(spacing: 8) {
ForEach(Array(tabs.enumerated()), id: \.offset) { index, title in
Button(title) {
withAnimation(.bouncy) {
selectedTab = index
}
}
.fontWeight(selectedTab == index ? .bold : .regular)
.foregroundStyle(selectedTab == index ? .white : .secondary)
.glassEffect(selectedTab == index ? .regular : .identity)
.glassEffectID("tab-\(index)", in: namespace)
}
}
}
}
}
struct ExpandableFAB: View {
@Namespace private var namespace
@State private var isOpen = false
var body: some View {
VStack(spacing: 12) {
GlassEffectContainer(spacing: 10) {
if isOpen {
Button(action: { takePhoto() }) {
Label("Camera", systemImage: "camera")
}
.glassEffect()
.glassEffectID("camera", in: namespace)
Button(action: { pickPhoto() }) {
Label("Photos", systemImage: "photo")
}
.glassEffect()
.glassEffectID("photos", in: namespace)
Button(action: { pickFile() }) {
Label("Files", systemImage: "doc")
}
.glassEffect()
.glassEffectID("files", in: namespace)
}
Button(action: {
withAnimation(.bouncy) {
isOpen.toggle()
}
}) {
Image(systemName: isOpen ? "xmark" : "plus")
.font(.title2)
.fontWeight(.bold)
.rotationEffect(.degrees(isOpen ? 90 : 0))
}
.glassEffect(.regular.interactive())
.glassEffectID("fab", in: namespace)
}
}
}
}
.bouncy or .spring animations for the best morphing feelglassEffectIDiOS 26 introduces two glass-specific button styles.
.glass (Translucent)// ✅ Secondary action -- translucent glass
Button("Cancel") {
cancel()
}
.buttonStyle(.glass)
Use .glass for secondary actions, navigation controls, and non-primary interactions.
.glassProminent (Opaque)// ✅ Primary action -- opaque glass
Button("Continue") {
proceed()
}
.buttonStyle(.glassProminent)
Use .glassProminent for primary actions, call-to-action buttons, and confirmations.
| Style | Opacity | Use Case |
|-------|---------|----------|
| .glass | Translucent | Secondary, cancel, navigation, toolbar |
| .glassProminent | Opaque | Primary, submit, continue, call-to-action |
// ✅ Using buttonStyle with glassEffect for fine control
Button("Share") { share() }
.buttonStyle(.glass)
.glassEffect(.regular.tint(.blue).interactive())
// ✅ Prominent with tint
Button("Purchase") { purchase() }
.buttonStyle(.glassProminent)
.glassEffect(.regular.tint(.green))
Content rendered on or through glass requires careful treatment for legibility.
// ✅ High-contrast text on glass
Text("Navigation Title")
.font(.headline)
.fontWeight(.bold)
.foregroundStyle(.white)
// ✅ Secondary text with reduced emphasis
Text("Subtitle")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.white.opacity(0.8))
// ❌ Light/thin text on glass -- poor legibility
Text("Hard to Read")
.font(.caption)
.fontWeight(.light)
.foregroundStyle(.gray)
// ✅ Icon-only label on glass
Label("Settings", systemImage: "gear")
.labelStyle(.iconOnly)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(.white)
// ✅ SF Symbol with proper weight
Image(systemName: "magnifyingglass")
.font(.body.weight(.bold))
.foregroundStyle(.white)
// ✅ Proper padding for glass content
HStack(spacing: 8) {
Image(systemName: "bell.fill")
Text("Notifications")
.fontWeight(.semibold)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.foregroundStyle(.white)
.glassEffect()
.bold or .semibold font weights minimum.foregroundStyle(.white) or .primary for text on glass.thin, .ultraLight, or .light weights on glassGlass exists exclusively in the navigation layer. This is a strict architectural boundary in iOS 26.
+----------------------------------+
| Glass Layer (Navigation) | <- Tab bars, toolbars, nav bars,
| Floating above content | floating buttons, menus
+----------------------------------+
| |
| Content Layer | <- Lists, cards, images, text,
| Your app's actual content | media, forms, data
| |
+----------------------------------+
// ✅ Glass only on navigation elements
struct ContentView: View {
var body: some View {
NavigationStack {
// Content layer -- NO glass
ScrollView {
LazyVStack(spacing: 16) {
ForEach(items) { item in
ItemCard(item: item) // No glass here
}
}
}
// Navigation layer -- glass is automatic in toolbars
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(action: { addItem() }) {
Image(systemName: "plus")
}
}
}
}
}
}
// ❌ WRONG: Glass applied to content elements
struct BadContentView: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
ForEach(items) { item in
// ❌ Glass on content cards -- violates navigation layer rule
HStack {
Text(item.title)
Spacer()
Text(item.subtitle)
}
.padding()
.glassEffect() // ❌ Do NOT do this
}
}
}
}
}
In iOS 26, these navigation elements receive glass treatment automatically:
NavigationStack title barsTabView tab bars.toolbar items.navigationBarItems// ✅ Custom floating glass button overlaying content
ZStack(alignment: .bottomTrailing) {
// Content layer
ScrollView {
ContentGrid()
}
// Navigation layer -- floating glass FAB
Button(action: { createNew() }) {
Image(systemName: "plus")
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(.white)
.padding(18)
}
.glassEffect(.regular.interactive())
.padding(24)
}
Scroll edge effects control how the glass navigation bar transitions as the user scrolls content beneath it.
// ✅ Soft scroll edge -- subtle, gradual transition
NavigationStack {
ScrollView {
ContentView()
}
.toolbarBackgroundVisibility(.automatic, for: .navigationBar)
}
Soft edges create a gentle, blurred transition between the glass navigation bar and scrolling content. This is the default and preferred style for iOS and iPadOS.
// ✅ Hard scroll edge -- stronger boundary
NavigationStack {
ScrollView {
ContentView()
}
.toolbarBackgroundVisibility(.visible, for: .navigationBar)
}
Hard edges create a more defined, opaque boundary. This is the default on macOS where the visual language favors stronger delineation.
Liquid Glass includes comprehensive automatic accessibility adaptations. These require no code from you -- the system handles them.
| Setting | Adaptation | |---------|-----------| | Reduce Transparency | Glass frosting increases to near-opaque, background blur intensifies | | Increase Contrast | Glass gains stark borders and higher-contrast fills | | Reduce Motion | Morphing animations, shimmer, bounce, and interactive effects are toned down or disabled | | Tinted Mode (iOS 26.1+) | Glass applies user-selected tint overlays |
For cases where automatic adaptations are insufficient:
struct AccessibleGlassView: View {
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
Button("Action") { performAction() }
.glassEffect(reduceTransparency ? .identity : .regular)
.animation(reduceMotion ? .none : .bouncy, value: someState)
}
}
struct AdaptiveToolbar: View {
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
var body: some View {
HStack(spacing: 12) {
Button("Back") { goBack() }
Spacer()
Button("Done") { done() }
}
.padding()
.background {
if reduceTransparency {
// ✅ Solid fallback when transparency is reduced
RoundedRectangle(cornerRadius: 16)
.fill(.regularMaterial)
}
}
.glassEffect(reduceTransparency ? .identity : .regular)
}
}
Glass automatically adapts to the current color scheme. No additional code is required for basic adaptation.
| Property | Light Mode | Dark Mode | |----------|-----------|-----------| | Frosting | Lighter, subtle blur | Deeper, richer blur | | Shadows | Soft, light shadows | Muted, ambient shadows | | Specular highlights | Bright, crisp | Subtle luminance glow | | Background sampling | Lighter refraction | Darker refraction | | Tint rendering | Lighter wash | Deeper, richer color |
// ✅ Preview in both color schemes
struct GlassButton_Previews: PreviewProvider {
static var previews: some View {
Group {
GlassButton()
.preferredColorScheme(.light)
.previewDisplayName("Light Mode")
GlassButton()
.preferredColorScheme(.dark)
.previewDisplayName("Dark Mode")
}
.padding()
.background(Color.blue.gradient)
}
}
// ✅ Adjusting tint intensity based on color scheme
struct AdaptiveGlassButton: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Button("Subscribe") { subscribe() }
.glassEffect(
.regular.tint(
colorScheme == .dark
? .blue.opacity(0.4)
: .blue.opacity(0.6)
)
)
}
}
Glass is available across all Apple platforms in iOS 26 and its equivalents, but the rendering and default behaviors differ by platform.
| Platform | Default Shape | Notes | |----------|--------------|-------| | iOS | Capsule | Full interactive support, touch-point illumination | | iPadOS | Capsule | Larger touch targets, pointer hover support | | macOS | Platform-specific | Different corner radii, hover effects, click behaviors | | watchOS | Rounded rectangle | Smaller glass areas, simplified effects | | tvOS | Rounded rectangle | Focus-based interaction, large glass surfaces | | visionOS | Spatial glass | 3D depth, spatial lighting, volumetric rendering |
// ✅ Platform-adaptive glass toolbar
struct CrossPlatformToolbar: View {
var body: some View {
HStack(spacing: 12) {
Button(action: { goBack() }) {
Image(systemName: "chevron.left")
}
Spacer()
Button(action: { share() }) {
Image(systemName: "square.and.arrow.up")
}
}
.padding()
#if os(iOS)
.glassEffect(.regular.interactive())
#elseif os(macOS)
.glassEffect(.regular)
#elseif os(visionOS)
.glassEffect(.regular)
#else
.glassEffect()
#endif
}
}
// ✅ visionOS spatial glass considerations
#if os(visionOS)
struct SpatialGlassPanel: View {
var body: some View {
VStack(spacing: 16) {
Text("Controls")
.font(.headline)
Button("Play") { play() }
.glassEffect()
}
.padding(24)
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 24))
// Spatial glass uses real environment lighting
// and has true depth in 3D space
}
}
#endif
// ✅ Use SF Symbols consistently across platforms
Label("Share", systemImage: "square.and.arrow.up")
.labelStyle(.iconOnly)
.font(.body.weight(.semibold))
.foregroundStyle(.white)
.glassEffect()
// SF Symbols render correctly on all platforms
// and adapt to platform-specific rendering
Glass blur compositing is computationally expensive. Follow these guidelines to maintain smooth performance.
// ✅ Batch glass elements in a container (single compositing pass)
GlassEffectContainer(spacing: 12) {
ForEach(actions) { action in
Button(action.title) { action.perform() }
.glassEffect()
}
}
// ❌ Individual glass elements without a container (multiple compositing passes)
ForEach(actions) { action in
Button(action.title) { action.perform() }
.glassEffect()
}
| Strategy | Description |
|----------|-------------|
| Use GlassEffectContainer | Batches multiple glass shapes into a single compositing pass |
| Avoid glass on scroll cells | Glass on every cell in a LazyVStack/List is extremely expensive |
| Limit glass count | Keep total visible glass elements under 8-10 per screen |
| Use .identity off-screen | Disable glass for views that are not visible |
| Avoid glass on animated content | Do not apply glass to views with continuous animation |
| Profile with Instruments | Use the Rendering instrument to identify GPU bottlenecks |
| Test on oldest target device | Glass is most expensive on older GPUs (A12, A13) |
// ✅ Glass only on visible navigation, NOT on list content
struct PerformantListView: View {
var body: some View {
NavigationStack {
List(items) { item in
// ✅ Content cells have NO glass
ItemRow(item: item)
}
.toolbar {
// ✅ Glass only on toolbar controls
ToolbarItem(placement: .topBarTrailing) {
Button(action: { addItem() }) {
Image(systemName: "plus")
}
}
}
}
}
}
// Profile glass performance with Instruments:
// 1. Open Instruments > Rendering template
// 2. Look for "Offscreen Render" passes (glass causes these)
// 3. Check GPU utilization -- glass should not push above 60%
// 4. Monitor frame drops in scroll views near glass elements
// 5. Compare with glass disabled (.identity) to measure impact
struct MediaOverlayControls: View {
@Namespace private var controlsNamespace
@State private var isExpanded = false
@State private var isPlaying = false
var body: some View {
ZStack(alignment: .bottom) {
// Content layer -- full-screen media
AsyncImage(url: mediaURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Color.black
}
.ignoresSafeArea()
// Navigation layer -- glass controls
GlassEffectContainer(spacing: 12) {
HStack(spacing: 16) {
if isExpanded {
Button(action: { skipBackward() }) {
Image(systemName: "gobackward.15")
.font(.title3)
}
.glassEffect(.clear.interactive())
.glassEffectID("backward", in: controlsNamespace)
}
Button(action: {
withAnimation(.bouncy) {
isPlaying.toggle()
}
}) {
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.font(.title)
.fontWeight(.bold)
}
.glassEffect(.clear.interactive())
.glassEffectID("playPause", in: controlsNamespace)
if isExpanded {
Button(action: { skipForward() }) {
Image(systemName: "goforward.15")
.font(.title3)
}
.glassEffect(.clear.interactive())
.glassEffectID("forward", in: controlsNamespace)
}
}
.foregroundStyle(.white)
}
.padding(.bottom, 60)
.onTapGesture {
withAnimation(.bouncy) {
isExpanded.toggle()
}
}
}
}
}
struct GlassSettingsToolbar: View {
@Namespace private var settingsNamespace
@State private var activePanel: SettingsPanel? = nil
enum SettingsPanel: String, CaseIterable {
case display = "Display"
case audio = "Audio"
case network = "Network"
}
var body: some View {
VStack(spacing: 16) {
// Glass tab selector
GlassEffectContainer(spacing: 8) {
HStack(spacing: 8) {
ForEach(SettingsPanel.allCases, id: \.self) { panel in
Button(panel.rawValue) {
withAnimation(.bouncy) {
activePanel = (activePanel == panel) ? nil : panel
}
}
.fontWeight(activePanel == panel ? .bold : .regular)
.foregroundStyle(activePanel == panel ? .white : .secondary)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.glassEffect(
activePanel == panel
? .regular.tint(.blue)
: .clear
)
.glassEffectID(panel.rawValue, in: settingsNamespace)
}
}
}
// Content area (no glass)
if let panel = activePanel {
SettingsPanelContent(panel: panel)
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
}
}
}
struct AccessibleGlassNavBar: View {
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Namespace private var navNamespace
@State private var selectedTab = 0
let tabs: [(String, String)] = [
("Home", "house.fill"),
("Search", "magnifyingglass"),
("Favorites", "heart.fill"),
("Profile", "person.fill")
]
var body: some View {
GlassEffectContainer(spacing: 4) {
HStack(spacing: 4) {
ForEach(Array(tabs.enumerated()), id: \.offset) { index, tab in
Button(action: {
let animation: Animation = reduceMotion ? .none : .bouncy
withAnimation(animation) {
selectedTab = index
}
}) {
VStack(spacing: 4) {
Image(systemName: tab.1)
.font(.body.weight(.semibold))
Text(tab.0)
.font(.caption2)
.fontWeight(.medium)
}
.foregroundStyle(selectedTab == index ? .white : .secondary)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
}
.accessibilityLabel(tab.0)
.accessibilityAddTraits(selectedTab == index ? .isSelected : [])
.glassEffect(
selectedTab == index
? (reduceTransparency ? .identity : .regular)
: .identity
)
.glassEffectID("nav-\(index)", in: navNamespace)
}
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
| # | Mistake | Fix |
|---|---------|-----|
| 1 | Applying glass to content cards or list rows | Glass is for navigation layer only. Use .background(.regularMaterial) for content surfaces. |
| 2 | Missing GlassEffectContainer around multiple glass elements | Wrap 2+ adjacent glass elements in GlassEffectContainer(spacing:) to prevent glass-on-glass artifacts. |
| 3 | Using .regular over media-rich backgrounds | Use .clear for high-transparency glass over photos, maps, and video. |
| 4 | Poor text contrast on glass | Use .foregroundStyle(.white) and .fontWeight(.bold) or .semibold minimum. |
| 5 | Missing accessibility support | Glass adapts automatically, but test with Reduce Transparency, Increase Contrast, and Reduce Motion. Add manual overrides when automatic adaptation is insufficient. |
| 6 | Morphing without GlassEffectContainer | All morphing elements must share the same GlassEffectContainer and Namespace. |
| 7 | Morphing without animation wrapper | State changes must be wrapped in withAnimation(.bouncy) or similar for morphing to animate. |
| 8 | Glass on every cell in a scrolling list | Extremely expensive. Glass compositing on 50+ cells causes frame drops. Keep glass on fixed navigation elements only. |
| 9 | Using .interactive() on non-tappable views | Interactive glass is for buttons and controls only. Applying it to labels or decorations is misleading. |
| 10 | Forgetting to test on older devices | Glass compositing is GPU-intensive. Test on the oldest supported device (e.g., iPhone XR / A12) to ensure 60fps. |
| 11 | Using thin/light font weights on glass | Thin text is illegible through glass. Use .semibold or .bold minimum. |
| 12 | Not providing glassEffectID for all morphing elements | Every element in a morphing group needs a unique, stable ID. Missing IDs cause abrupt appearance/disappearance instead of morphing. |
| 13 | Mixing soft and hard scroll edges | Choose one scroll edge style per view. Mixing creates visual inconsistency. |
| 14 | Applying glass with too little padding | Glass needs breathing room. Minimum 10pt vertical and 14pt horizontal padding for legibility and touch targets. |
| 15 | Using glass for decorative purposes | Glass communicates interactivity. Decorative use dilutes its meaning and confuses users. |
Before designing your glass UI, look at real-world Liquid Glass implementations for inspiration.
.ultraThinMaterial / .regularMaterial// Before (iOS 15-17)
Button("Action") { performAction() }
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
// After (iOS 26+)
Button("Action") { performAction() }
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 16))
// Before (custom blur overlay)
ZStack {
content
Rectangle()
.fill(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay {
HStack { /* controls */ }
}
}
// After (glass effect)
ZStack {
content
HStack { /* controls */ }
.padding()
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 16))
}
// ✅ Backward-compatible glass usage
if #available(iOS 26, *) {
Button("Action") { performAction() }
.glassEffect(.regular)
} else {
Button("Action") { performAction() }
.background(.regularMaterial, in: Capsule())
}
// Basic glass
.glassEffect()
.glassEffect(.regular)
.glassEffect(.clear)
.glassEffect(.identity)
// Tinting
.glassEffect(.regular.tint(.blue))
.glassEffect(.clear.tint(.red.opacity(0.5)))
// Interactive (iOS only)
.glassEffect(.regular.interactive())
.glassEffect(.clear.interactive().tint(.green))
// Shapes
.glassEffect(.regular, in: .capsule)
.glassEffect(.regular, in: .circle)
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 16))
.glassEffect(.regular, in: .rect(cornerRadius: .containerConcentric))
.glassEffect(.regular, in: .ellipse)
// Conditional
.glassEffect(.regular, isEnabled: condition)
.glassEffect(condition ? .regular : .identity)
// Container
GlassEffectContainer(spacing: 12) { /* glass views */ }
// Morphing
.glassEffectID("uniqueID", in: namespace)
// Button styles
.buttonStyle(.glass)
.buttonStyle(.glassProminent)
Need glass?
|
+-- Is it a navigation element? (toolbar, tab bar, nav bar, FAB)
| YES -> Use .glassEffect()
| NO -> Do NOT use glass. Use .background(.regularMaterial) if needed.
|
+-- Over media-rich content? (photos, video, maps)
| YES -> Use .clear
| NO -> Use .regular
|
+-- Is it a tappable control?
| YES -> Consider .interactive() (iOS only)
| NO -> Do NOT use .interactive()
|
+-- Multiple glass elements adjacent?
| YES -> Wrap in GlassEffectContainer
| NO -> No container needed
|
+-- Need animated transitions between glass shapes?
YES -> Use GlassEffectContainer + glassEffectID + Namespace + withAnimation
NO -> Standard .glassEffect() is sufficient
data-ai
SQLDelight patterns for Kotlin Multiplatform - .sq file definitions, platform drivers, type adapters, migrations, and shared database access.
data-ai
Room database patterns for Android - entity definitions, DAO interfaces, Database class, migrations, TypeConverters, and Flow integration.
tools
Push notification patterns - FCM setup for Android, APNs for iOS, notification channels, payload handling, foreground/background behavior, and rich notifications.
content-media
Pagination patterns for mobile - Paging 3 for Android (PagingSource, RemoteMediator, LazyPagingItems), cursor-based and offset-based strategies.