```html SwiftUI: How to Implement Reduced Motion (iOS 17+, 2026)

How to implement reduced motion in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: accessibilityReduceMotion Updated: May 11, 2026
TL;DR

Read @Environment(\.accessibilityReduceMotion) inside your view and pass nil instead of an Animation when it is true. That single swap respects the user's System Settings → Accessibility → Motion preference with zero extra dependencies.

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

    var body: some View {
        Circle()
            .fill(.red)
            .frame(width: 24, height: 24)
            .scaleEffect(scaled ? 1.3 : 1.0)
            .onAppear {
                guard !reduceMotion else { return }
                withAnimation(.easeInOut(duration: 0.8).repeatForever()) {
                    scaled = true
                }
            }
    }
}

Full implementation

The pattern below wraps the environment value in a small helper extension so you can call .animation(reduceMotion ? nil : myAnimation, value:) everywhere in your app without repeating the conditional. The example demonstrates a card that slides in on appearance and a toggle that controls a colour change — both honour the user's motion preference.

import SwiftUI

// MARK: - Helpers

extension Animation {
    /// Returns nil when the user has enabled Reduce Motion,
    /// otherwise returns `self`. Attach with .animation(myAnim.ifAllowed, value:)
    func ifAllowed(reduceMotion: Bool) -> Animation? {
        reduceMotion ? nil : self
    }
}

// MARK: - Demo screen

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

    @State private var isVisible   = false
    @State private var isHighlight = false

    /// Centralise the motion-aware animation so it's easy to update.
    private var slideAnimation: Animation? {
        Animation.spring(response: 0.5, dampingFraction: 0.7)
            .ifAllowed(reduceMotion: reduceMotion)
    }

    var body: some View {
        VStack(spacing: 32) {

            // 1. Animated card that slides in from the bottom
            if isVisible {
                RoundedRectangle(cornerRadius: 20)
                    .fill(isHighlight ? Color.indigo : Color.blue)
                    .frame(height: 140)
                    .overlay(
                        Text(reduceMotion ? "Motion reduced ✓" : "Slide animation")
                            .font(.headline)
                            .foregroundStyle(.white)
                    )
                    .transition(
                        reduceMotion
                            ? .identity           // instant — no movement
                            : .move(edge: .bottom).combined(with: .opacity)
                    )
            }

            // 2. Toggle that triggers a colour change
            Toggle("Highlight card", isOn: $isHighlight)
                .padding(.horizontal)
                .animation(
                    Animation.easeInOut(duration: 0.3)
                        .ifAllowed(reduceMotion: reduceMotion),
                    value: isHighlight
                )

            // 3. Appear / disappear button
            Button(isVisible ? "Hide card" : "Show card") {
                withAnimation(slideAnimation) {
                    isVisible.toggle()
                }
            }
            .buttonStyle(.borderedProminent)

            // 4. Status label for debugging
            Text(reduceMotion
                 ? "Reduce Motion: ON — animations disabled"
                 : "Reduce Motion: OFF — animations enabled")
                .font(.caption)
                .foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
                .padding(.horizontal)
        }
        .padding()
        .navigationTitle("Reduced Motion")
        .onAppear {
            withAnimation(slideAnimation) {
                isVisible = true
            }
        }
    }
}

// MARK: - Preview

#Preview("Reduce Motion OFF") {
    NavigationStack {
        ReducedMotionDemoView()
    }
}

#Preview("Reduce Motion ON") {
    NavigationStack {
        ReducedMotionDemoView()
            .environment(\.accessibilityReduceMotion, true)
    }
}

How it works

  1. @Environment(\.accessibilityReduceMotion) — SwiftUI injects the current value of the OS "Reduce Motion" toggle (Settings → Accessibility → Motion) directly into your view hierarchy. It updates automatically when the user changes the setting, causing affected views to re-render.
  2. Animation.ifAllowed(reduceMotion:) — The helper extension converts any Animation into an optional. Passing nil to .animation(_:value:) or withAnimation(_:) makes SwiftUI apply the state change instantly with no interpolation, which is exactly what users with vestibular disorders need.
  3. .transition(.identity) vs .move(edge:) — Transitions are separate from animations in SwiftUI. When reduceMotion is true, the card uses .identity (appear/disappear in place), bypassing the bottom-slide movement that can trigger motion sickness.
  4. Inline status label — The Text at the bottom surfaces the current environment value so you can verify the setting during development. Remove it (or wrap it in #if DEBUG) before shipping.
  5. Two #Preview macros — Using .environment(\.accessibilityReduceMotion, true) on the second preview lets you QA both states without touching device Settings, shortcutting the review cycle inside Soarias.

Variants

App-wide animation modifier via ViewModifier

If you want every view in the app to honour reduced motion without reading the environment individually, apply a ViewModifier at the root and let it propagate.

struct ReducedMotionModifier: ViewModifier {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    func body(content: Content) -> some View {
        content
            // Overrides the transaction for the entire subtree
            .transaction { transaction in
                if reduceMotion {
                    transaction.animation = nil
                    transaction.disablesAnimations = true
                }
            }
    }
}

extension View {
    func respectsReducedMotion() -> some View {
        modifier(ReducedMotionModifier())
    }
}

// Usage in your root view:
// ContentView()
//     .respectsReducedMotion()

Crossfade instead of no animation

Some designers prefer a subtle opacity fade over a hard cut because it feels intentional rather than broken. Replace nil with .easeIn(duration: 0.15) only for opacity-based transitions — crossfades involve no spatial movement, so they are generally safe for motion-sensitive users. Consult your accessibility auditor before shipping this variant, as guidelines can vary per context.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement reduced motion support in SwiftUI for iOS 17+.
Use accessibilityReduceMotion from the SwiftUI environment.
Replace all spring/easeInOut animations with nil when reduceMotion is true.
Replace .move() and .scale() transitions with .identity.
Make it accessible (VoiceOver labels on all interactive elements).
Add a #Preview with realistic sample data and a second #Preview
that injects .environment(\.accessibilityReduceMotion, true).

Paste this prompt in Soarias's Build phase after your screen scaffolding is in place — Claude Code will audit every animated modifier in the file and patch them in one pass.

Related

FAQ

Does this work on iOS 16?

Yes — \.accessibilityReduceMotion has been available since iOS 13. However, the #Preview macro requires Xcode 15 / iOS 17 SDK to compile. If you need to support iOS 16, replace #Preview with struct MyView_Previews: PreviewProvider and inject the environment value there instead.

How do I test reduced motion in the simulator?

Go to Settings → Accessibility → Motion → Reduce Motion on your simulator or device and toggle it on. Alternatively, the fastest approach during development is the second #Preview shown in the full implementation above — it injects .environment(\.accessibilityReduceMotion, true) so you can see both states side by side in Xcode's canvas without touching Settings at all.

What's the UIKit equivalent?

In UIKit, check UIAccessibility.isReduceMotionEnabled (a static Bool) and observe UIAccessibility.reduceMotionStatusDidChangeNotification via NotificationCenter to react to live changes. SwiftUI's environment value does all of this automatically, updating your view whenever the setting changes without any manual observation code.

Last reviewed: 2026-05-11 by the Soarias team.

```