KahWee

Thoughts on web development, programming, and technology

SwiftUI Liquid Glass and Accessibility - Reduce Motion, VoiceOver, and Dynamic Type in iOS 26

I started experimenting with SwiftUI to build a UI using iOS 26's new Liquid Glass design language.glassEffect() modifiers, blur animations, refraction, and parallax. The truth is I kinda find the whole iOS glass effect too distracting after a while and wanted to tone it down. I wanted to make sure Reduce Motion worked properly, and fell into a rabbit hole of iOS 26 accessibility documentation.

Here's what I learned about making Liquid Glass effects accessible, with validated examples and official Apple documentation links.

What is Liquid Glass?

iOS 26 introduces Liquid Glass, Apple's new unified design language influenced by visionOS. It replaces the flat design from iOS 7 with rounded, translucent elements that have "optical qualities of glass" including refraction, blur, and dynamic lighting that react to motion, content, and user input.

Key visual properties:

  • Environmental refraction - Refracts and reflects content behind it
  • Dynamic light reflection - Responds to light sources with realistic shading
  • Responsive fluidity - Morphs and flexes based on size and interaction
  • Adaptive blur - System-defined blur that adapts to background content

When glass elements morph to larger sizes, the material simulates thicker glass with deeper shadows, more pronounced lensing, and softer light scattering.

Why Accessibility Matters with Liquid Glass

iOS 26's Liquid Glass design is visually striking, but the blur, refraction, and parallax effects can cause accessibility issues if not handled properly. The three critical areas are:

  1. Reduce Motion - Disable or simplify Liquid Glass animations
  2. Reduce Transparency - Make glass effects less translucent for better contrast
  3. VoiceOver - Ensure glass-styled components have proper labels
  4. Dynamic Type - Support scalable text on glass backgrounds

How iOS 26 Handles Liquid Glass Accessibility

Apple built accessibility support directly into Liquid Glass:

Reduce Motion (Settings → Accessibility → Motion → Reduce Motion):

  • Decreases the intensity of glass effects
  • Disables elastic properties of the material
  • Removes parallax and refraction animations

Reduce Transparency (Settings → Accessibility → Display & Text Size → Reduce Transparency):

  • Makes Liquid Glass "frostier" and more opaque
  • Obscures more content behind glass elements
  • Improves contrast for low-vision users

Combined settings (Reduce Transparency + Reduce Motion + Dark Mode):

  • Delivers the best visibility, performance, and battery life
  • Makes Liquid Glass behave more like iOS 25's flat design

Source: MacRumors - iOS 26: Reduce Transparency of Apple's Liquid Glass Design

1. Reduce Motion - Disable Liquid Glass Animations

When users enable Reduce Motion, apps should disable or simplify Liquid Glass blur animations, parallax, refraction, and elastic morphing effects.

Why it matters: Liquid Glass's dynamic refraction, parallax scrolling, and morphing animations can cause motion sickness for users with vestibular disorders.

Implementation

struct AnimatedBadge: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var animate = false

    var body: some View {
        let animation = reduceMotion
            ? .none
            : .easeInOut(duration: 0.8).repeatForever(autoreverses: true)

        Circle()
            .fill(Color.blue)
            .frame(width: 12, height: 12)
            .scaleEffect(animate ? 1.2 : 1.0)
            .onAppear {
                guard !reduceMotion else { return }
                withAnimation(animation) {
                    animate = true
                }
            }
    }
}

The pattern is simple:

  • Use @Environment(\.accessibilityReduceMotion) to read the system setting
  • Set animation to .none when Reduce Motion is enabled
  • Early return in onAppear prevents state changes when motion is reduced

What to Disable with Liquid Glass

When Reduce Motion is enabled, disable or simplify:

  • .glassEffect() morphing and elastic animations
  • Parallax scrolling behind glass surfaces
  • Animated blur intensity changes
  • Refraction animation as content moves behind glass
  • Particle effects and physics-driven glass shards
  • Large-scale transformations on glass elements

Using Liquid Glass with Reduce Motion

struct GlassCard: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    var body: some View {
        VStack {
            Text("Content")
        }
        .padding()
        .background {
            if reduceMotion {
                // Fallback: simple blur without morphing
                RoundedRectangle(cornerRadius: 16)
                    .fill(.ultraThinMaterial)
            } else {
                // Full Liquid Glass effect
                RoundedRectangle(cornerRadius: 16)
                    .glassEffect(.regular.interactive())
            }
        }
    }
}

When Reduce Motion is enabled, replace .glassEffect() with SwiftUI's simpler .ultraThinMaterial or .regularMaterial backgrounds. You get the blur without the dynamic morphing and refraction.

Official documentation: accessibilityReduceMotion

Official documentation: Applying Liquid Glass to custom views

2. Dynamic Type - Support Scalable Text

Dynamic Type allows users to adjust text size system-wide. SwiftUI text styles (.headline, .body, .caption) scale automatically, but you need to handle layout constraints.

Why it matters: Users with visual impairments rely on larger text sizes (up to accessibility size categories like accessibility5).

Implementation

Text(item.name)
    .font(.headline)
    .fontWeight(.semibold)
    .lineLimit(2)
    .multilineTextAlignment(.leading)
    .dynamicTypeSize(...DynamicTypeSize.accessibility5)

What matters:

  • Use SwiftUI's built-in text styles (.headline, .body, .title2) instead of fixed point sizes
  • The range operator ...DynamicTypeSize.accessibility5 caps the maximum text size
  • Allow at least 2 lines of text to prevent truncation at larger sizes
  • Test layouts with Settings → Accessibility → Display & Text Size → Larger Text

Capping Dynamic Type

Sometimes unlimited scaling breaks layouts. You can cap at a maximum size:

Text("Fixed Layout Text")
    .font(.body)
    .dynamicTypeSize(.medium...DynamicTypeSize.xxxLarge)

This allows scaling up to xxxLarge but prevents accessibility sizes that might break the design.

Official documentation: DynamicTypeSize

3. 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.

Why it matters: 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 ?? "")

The modifiers:

  • .accessibilityElement(children: .combine) - Merge child elements into one
  • .accessibilityLabel() - Primary text read immediately by VoiceOver (keep it short)
  • .accessibilityHint() - Secondary context read after a delay
  • .accessibilityValue() - Current state or value (e.g., "3 items", "Enabled")

Example: 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 proper grouping:

"Favorites: 42"

Official documentation: Accessibility modifiers

iOS 26 Liquid Glass Accessibility Improvements

New in iOS 26:

  • .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() modifier makes glass respond to touch and focus with subtle lighting (disabled when Reduce Motion is on)

Liquid Glass Best Practices:

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

Watch: WWDC 2025 - Meet Liquid Glass

Read: Adopting Liquid Glass - Official Apple Documentation

Testing Your Liquid Glass Implementation

Reduce Motion:

  1. Settings → Accessibility → Motion → Reduce Motion → ON
  2. Verify .glassEffect() is replaced with static materials
  3. Check that parallax and refraction animations are disabled

Reduce Transparency:

  1. Settings → Accessibility → Display & Text Size → Reduce Transparency → ON
  2. Verify glass elements become more opaque and frosty
  3. Check that content behind glass is more obscured for better contrast

Dynamic Type:

  1. Settings → Accessibility → Display & Text Size → Larger Text
  2. Drag slider to maximum (accessibility sizes)
  3. Check that text on glass backgrounds remains readable and layouts don't break

VoiceOver:

  1. Settings → Accessibility → VoiceOver → Enable
  2. Navigate through glass UI elements
  3. Listen for meaningful descriptions (not just "Glass effect, Button")

Combined Testing (recommended):
Enable Reduce Motion + Reduce Transparency + Dark Mode to test the most accessibility-friendly configuration.

My Take

I went into this wanting to build a polished UI with iOS 26's Liquid Glass design—.glassEffect(), refraction, blur animations, parallax scrolling. What I learned is that accessibility isn't an afterthought—it's baked into Liquid Glass from the start, but you have to explicitly respect it in custom implementations.

The @Environment(\.accessibilityReduceMotion) pattern is simple and effective. When Reduce Motion is enabled, I replace .glassEffect() with .ultraThinMaterial for a static blur without the morphing animations. VoiceOver grouping with .accessibilityElement(children: .combine) solved the problem of 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 looks incredible, but it's visually intense. Some users find it distracting or hard to read, which is why iOS 26 includes settings to tone it down. Testing with Reduce Motion + Reduce Transparency showed me how much the system adapts—glass becomes more opaque, animations stop, and the UI feels closer to iOS 25's flat design.

What I'd do differently next time:

  1. Check accessibilityReduceMotion and fallback to .ultraThinMaterial instead of .glassEffect()
  2. Use SwiftUI text styles (.headline, .body) with dynamicTypeSize ranges for text on glass backgrounds
  3. Group compound glass views with accessibilityElement(children: .combine)
  4. Test with both Reduce Motion and Reduce Transparency enabled
  5. Don't manually combine .glassEffect() with .blur() or .opacity()—let the system handle it

The iOS 26 Liquid Glass design is visually impressive and accessibility is built-in. The hard part isn't the API—it's remembering to test with Reduce Motion and Reduce Transparency enabled.

All Tags