Technical guide
25

Technical guide · Published Oct 9, 2025

What VoiceOver Exposed in My Liquid Glass UI

VoiceOver read my compound views as a pile of unrelated elements. Grouping, labels, and combined accessibility testing fixed the experience.

A field-guide drawing of a Pacific banana slug
In this article4 sections

VoiceOver read my compound views as a pile of unrelated images and text. The fix was not specific to Liquid Glass, but the visual styling had made it easy to miss. This is Part 2 on grouping, labels, and testing. Part 1 covers Reduce Motion and Dynamic Type.

VoiceOver Labels, Hints, and Grouping

VoiceOver reads UI elements aloud for blind and low-vision users. SwiftUI provides default labels for text and images, but custom views need explicit accessibility annotations. Without proper labels, VoiceOver reads raw view hierarchies (“Image, Text, Text, Text”) instead of meaningful descriptions.

Combining elements: Group related views so VoiceOver reads them as one logical element:

HStack {
    Image(systemName: "photo")
        .resizable()
        .frame(width: 60, height: 60)

    VStack(alignment: .leading) {
        Text(item.name)
            .font(.headline)
        Text("\(item.quantity) items")
            .font(.subheadline)
        if let notes = item.notes {
            Text(notes)
                .font(.caption)
        }
    }
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(item.name), \(item.quantity) items")
.accessibilityHint(item.notes ?? "No notes")
.accessibilityValue(item.condition?.rawValue ?? "")

.accessibilityElement(children: .combine) merges child elements into one. .accessibilityLabel() is the primary text VoiceOver reads immediately—keep it short. .accessibilityHint() is secondary context read after a delay. .accessibilityValue() holds current state.

Stat Card with Icon:

VStack {
    Image(systemName: "star.fill")
        .font(.title)
    Text("\(count)")
        .font(.title2)
        .fontWeight(.bold)
    Text(label)
        .font(.caption)
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(label): \(count)")

Without .accessibilityElement(children: .combine), VoiceOver reads “Star, 42, Favorites”. With grouping: “Favorites: 42”.

iOS 26 Liquid Glass Defaults

.glassEffect() automatically respects Reduce Motion and Reduce Transparency system settings. Liquid Glass materials adapt opacity and blur intensity based on accessibility preferences. GlassEffectContainer groups glass views for shared rendering and better performance. .interactive() makes glass respond to touch and focus with subtle lighting—disabled when Reduce Motion is on.

Apply .glassEffect() to floating elements (toolbars, tab bars, cards). Avoid placing solid fills behind glass views. Don’t combine .glassEffect() with manual .blur() or .opacity() modifiers. Use GlassEffectContainer when multiple glass elements share the same space.

Testing Your Implementation

Reduce Motion: Settings -> Accessibility -> Motion -> Reduce Motion -> ON. Verify .glassEffect() is replaced with static materials. Check that parallax and refraction animations are disabled.

Reduce Transparency: Settings -> Accessibility -> Display & Text Size -> Reduce Transparency -> ON. Glass elements should become more opaque and frosty. Content behind glass should be more obscured for better contrast.

Dynamic Type: Settings -> Accessibility -> Display & Text Size -> Larger Text. Drag the slider to maximum. Check that text on glass backgrounds stays readable and layouts don’t break.

VoiceOver: Settings -> Accessibility -> VoiceOver -> Enable. Navigate through glass UI elements. Listen for meaningful descriptions, not just “Glass effect, Button”.

Combined testing: Enable Reduce Motion + Reduce Transparency + Dark Mode together for the most accessibility-friendly configuration.

What Changed in My Implementation

Liquid Glass responds to Apple’s accessibility settings, but my custom views still needed explicit work.

The @Environment(\.accessibilityReduceMotion) pattern is straightforward. When Reduce Motion is on, I replace .glassEffect() with .ultraThinMaterial for a static blur without morphing animations. VoiceOver grouping with .accessibilityElement(children: .combine) solved screen readers treating each glass element separately. Dynamic Type worked automatically once I switched from fixed font sizes to SwiftUI’s built-in text styles.

Liquid Glass is visually intense. Testing with Reduce Motion and Reduce Transparency together showed me how much the system adapts: glass becomes more opaque, animations stop, and the UI moves closer to the flatter iOS 25 look.

Next time I will start with those settings enabled. I will also group compound views before polishing their appearance, use SwiftUI text styles, and add a static material fallback anywhere I introduce custom motion.

Field tip Test with both Reduce Motion and Reduce Transparency enabled simultaneously. That combination reveals the most about how your glass UI degrades.

The hard part is not the API itself but remembering to test with those settings enabled.

One quick signal

Did this earn your time?

What was missing?

Thanks. That gives me something concrete to check.