Animation Only Feedback

Status: ready

Symptom

The user knows the action worked only because something moved. The product image flies into the cart, a checkmark bounces, a pin pulses. Nothing on screen changes in a way that stays.

User taps Add to cart.
The image flies to the cart icon.
The cart count doesn't change.
VoiceOver says nothing.

Where it shows up

Add to cart, favorites, payment success, upload completion, refresh, onboarding steps, drag and drop.

Why it blocks the task

A VoiceOver user doesn't see the animation. A user with Reduce Motion on may get no animation at all. A user who looked away for 400 ms missed it. None of them knows whether the item is in the cart, so they tap again and end up with two.

Animation can make success feel good. It can't be the only proof of success.

Root cause

The animation is the message.

  1. No state stays on screen after the animation ends.
  2. The count or value doesn't update.
  3. VoiceOver gets no result.
  4. The Reduce Motion branch removes the animation and with it the only feedback.

Before

Button("Add to cart") {
    add(tea)
    flyToCart = true
}

Fix

Verdict: every result has a state that stays: text, a count, a changed button. The animation is on top, and Reduce Motion only changes the animation.

Why: the state is there for everyone, whether they saw the motion or not. VoiceOver reads it when the user gets there, and gets one short announcement for the result.

@Environment(\.accessibilityReduceMotion) private var reduceMotion

Button("Add to cart") {
    add(tea)
    withAnimation(reduceMotion ? .easeInOut(duration: 0.2) : .spring) {
        added = true
    }
    AccessibilityNotification.Announcement("Added to cart. \(cart.count) items.").post()
}

if added {
    Label("Added to cart", systemImage: "checkmark.circle")   // shown with or without Reduce Motion
}

CartButton()
    .accessibilityValue("\(cart.count) items")

With Reduce Motion, the confirmation fades in instead of flying. The text is the same in both cases. The common mistake is the opposite: if reduceMotion { Text("Added") } else { animation }. That hides the confirmation from everyone who didn't turn the setting on, including VoiceOver users.

Progress: the value, not the bar animation.

ProgressView("Uploading receipt", value: progress)

What we don't do

  • We don't show the confirmation only when Reduce Motion is on.
  • We don't use an announcement as the only feedback. It's spoken once and gone. The state stays.

Check

Profile:   Reduce Motion off, and then on
Task:      add jasmine tea to the cart
Expected:  "Added to cart" is visible in both cases
           the cart count changes in both cases
           with Reduce Motion, no flying image
Profile:   VoiceOver
Task:      add an item, then move to the cart button
Expected:  one announcement "Added to cart. 3 items."
           the cart button reads "3 items" as its value

WCAG mapping

  1. 2.3.3 Animation from Interactions: interaction-triggered motion can be turned off.
  2. 4.1.3 Status Messages: the result is available without seeing it.
  3. 1.3.1 Info and Relationships: the result is not only visual.
  1. State as Value
  2. Focus Recovery
  3. Meaningful Announcement
  1. Chapter 2
  2. Chapter 6
  3. Chapter 10
  4. Chapter 17

Sources

  1. Apple Human Interface Guidelines: Motion
  2. Apple Developer: SwiftUI accessibility environment values
  3. Apple Developer: AccessibilityNotification
  4. W3C: Understanding SC 2.3.3 Animation from Interactions