Animations and motion can be dizzying for some app users. Let's fix that with these practical examples and following the motion accessibility guidelines outlined in this repo. Do you see something missing? Contact @amos_gyamfi and @stefanjblos on Twitter.
- Apple Developer Videos: Accessibility
- HIG: Accessibility
- WCAG: Motion specific section
- Apple Design Awards: Inclusivity section
Why use animations? | Example |
---|---|
Delight and playfulness (Duolingo) | |
State change: Hamburger to close icon | |
Draw user’s attention | |
Guidance: Replace telling with showing |
- Symbol Effects, Phase, Keyframe, Spring:
Types of Animations | Example |
---|---|
Programmatically initiated: Loading | |
User initiated: Gestural-based |
Implicit animation: .animation:
import SwiftUI
struct Implicit: View {
@State private var starting = false
@State private var ending = false
@State private var rotating = false
var body: some View {
VStack {
Circle()
.trim(from: starting ? 1/3 : 1/9, to: ending ? 2/5 : 1)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round))
.animation(.easeOut(duration: 1).delay(0.5).repeatForever(autoreverses: true), value: starting)
.animation(.easeInOut(duration: 1).delay(1).repeatForever(autoreverses: true), value: ending)
.frame(width: 50, height: 50)
.rotationEffect(.degrees(rotating ? 360 : 0))
.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: rotating)
.accessibilityLabel("Loading Animation")
.onAppear {
starting.toggle()
rotating.toggle()
ending.toggle()
}
Image(.bmcLogo)
} //
}
}
#Preview {
Implicit()
}
Explicit animation: withAnimation():
import SwiftUI
struct Explicit: View {
@State private var starting = false
@State private var ending = false
@State private var rotating = false
var body: some View {
VStack {
Circle()
.trim(from: starting ? 1/3 : 1/9, to: ending ? 2/5 : 1)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round))
.frame(width: 50, height: 50)
.rotationEffect(.degrees(rotating ? 360 : 0))
.accessibilityLabel("Loading Animation")
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotating.toggle()
}
withAnimation(.easeOut(duration: 1).delay(0.5).repeatForever(autoreverses: true)) {
starting.toggle()
}
withAnimation(.easeInOut(duration: 1).delay(1).repeatForever(autoreverses: true)) {
ending.toggle()
}
}
Image(.bmcLogo)
} //
}
}
#Preview {
Explicit()
}
The implicit and explicit code samples above result in the same animation
- Standard Easing: default, linear, easeIn, easeOut, easeInOut
- Timing Curves (custom): easeInOutBack
- Springs: bouncy, smooth, snappy. Visit Purposeful SwiftUI Animation to learn more.
--
- Frequent particle animations: Rain, clouds, slowly moving stars, thunder
- Parallax: Multi-speed & multi-direction (can cause mismatch)
- UIMotionEffect: Creates a perception of depth
- Fun to use but can be disorienting
- Can cause motion sickness
- Persistent background and foreground effects: Stars and clouds
- Autoplaying GIFs and videos: show play/
- pause buttons
- Background animations: Hide buttons
- Animated illustration: Play/pause controls
- (looping more than 5x)
- People with visual disabilities: Distracting and not useful
- Blinking: Can cause seizures
- Great example: iOS Screen recording animation (Dynamic Island)
- Replace flashing: With varieties of SF Symbol animations to convey information
- Provide a way to disable animations
- Great example: Animated bouncy reactions in Telegram
- Provide unnoticeable or reduced behavior for animations
- Does not mean removing all animations
- GIFs and videos: Use image image-switching technique
- App Store: Horizontal card scrolling animation
- Settings App: limit animations/motion in all apps
- Scaling and zooming animation: Throwing animation of launching an iOS app icon
- What?: SwiftUI NavigationLink -> Cross-fade transition
- Push segues with slide-in/out animations: UIs appearing/disappearing
NavigationLink {
PreJoinScreen()
} label: {
VStack(alignment: .leading) {
HStack {
Image(systemName: "person.circle.fill")
.font(.title2)
Spacer()
Image(systemName: "ellipsis")
.rotationEffect(.degrees(90))
}
Spacer()
Text("New Meeting")
}
.padding()
.frame(width: 160, height: 160)
.background(.ultraThinMaterial)
.cornerRadius(20)
}
- When to use: When there is no suitable replacement animation
- Enabled: Replaces sliding transitions with a subtle fade
- Use NavigationLink to get it for free
- Example: Settings App
- Settings App: Remove some animations for specific apps
- App Store: Autoplay animated images and video previews
- Example: Downloading Headspace
- Stitch Game: Solve number puzzles by filling out patterns
- Reduce Motion enabled: Disables all sudden movements
- Use Motion: In-app Settings
- Reduce Motion: Does not stop all problematic animations
- Control what should stop and not
- Great example: PCalc
Reduce Motion Off
Reduce Motion On
- Bouncy Transition: Reduce Motion Off
//
// ReduceMotionAnimationNil.swift
// Hamburger to Close
//
import SwiftUI
struct ReduceMotionAnimationSubtleFeel: View {
@State private var isRotating = false
@State private var isHidden = false
// Reduce Motion On
let subtleFeel = Animation.snappy
// Reduce Motion OFF
let bouncyFeel = Animation.bouncy(duration: 0.4, extraBounce: 0.2)
// Detect and respond to reduce motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
VStack(spacing: 14){
Rectangle() // top
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? 48 : 0), anchor: .leading)
Rectangle() // middle
.frame(width: 64, height: 10)
.cornerRadius(4)
.scaleEffect(isHidden ? 0 : 1, anchor: isHidden ? .trailing: .leading)
.opacity(isHidden ? 0 : 1)
Rectangle() // bottom
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? -48 : 0), anchor: .leading)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.accessibilityLabel("Menu and close icon transition")
.onTapGesture {
withAnimation(reduceMotion ? subtleFeel : bouncyFeel) {
isRotating.toggle()
isHidden.toggle()
}
}
}
}
#Preview {
ReduceMotionAnimationSubtleFeel()
.preferredColorScheme(.dark)
}
- Alternative animation duration: Short enough to make it unnoticeable (0 sec)
- Remove animation altogether: nil
//
// ReduceMotionAnimationNil.swift
// Hamburger to Close
//
import SwiftUI
struct ReduceMotionAnimationNil: View {
@State private var isRotating = false
@State private var isHidden = false
// Reduce Motion On
let subtleFeel = Animation.snappy
// Reduce Motion OFF
let bouncyFeel = Animation.bouncy(duration: 0.4, extraBounce: 0.2)
// Detect and respond to reduce motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
VStack(spacing: 14){
Rectangle() // top
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? 48 : 0), anchor: .leading)
Rectangle() // middle
.frame(width: 64, height: 10)
.cornerRadius(4)
.scaleEffect(isHidden ? 0 : 1, anchor: isHidden ? .trailing: .leading)
.opacity(isHidden ? 0 : 1)
Rectangle() // bottom
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? -48 : 0), anchor: .leading)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.accessibilityLabel("Menu and close icon transition")
.onTapGesture {
withAnimation(reduceMotion ? nil : bouncyFeel) {
isRotating.toggle()
isHidden.toggle()
}
}
}
}
#Preview {
ReduceMotionAnimationSubtleFeel()
.preferredColorScheme(.dark)
}
- Provide an alternative reduced behavior
- Example: Switch a bouncy hamburger/close icon with a gentle one
//
// ReduceMotionAnimationNil.swift
// Hamburger to Close
//
import SwiftUI
struct ReduceMotionAnimationSubtleFeel: View {
@State private var isRotating = false
@State private var isHidden = false
// Reduce Motion On
let subtleFeel = Animation.snappy
// Reduce Motion OFF
let bouncyFeel = Animation.bouncy(duration: 0.4, extraBounce: 0.2)
// Detect and respond to reduce motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
VStack(spacing: 14){
Rectangle() // top
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? 48 : 0), anchor: .leading)
Rectangle() // middle
.frame(width: 64, height: 10)
.cornerRadius(4)
.scaleEffect(isHidden ? 0 : 1, anchor: isHidden ? .trailing: .leading)
.opacity(isHidden ? 0 : 1)
Rectangle() // bottom
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? -48 : 0), anchor: .leading)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.accessibilityLabel("Menu and close icon transition")
.onTapGesture {
withAnimation(reduceMotion ? subtleFeel : bouncyFeel) {
isRotating.toggle()
isHidden.toggle()
}
}
}
}
#Preview {
ReduceMotionAnimationSubtleFeel()
.preferredColorScheme(.dark)
}
- Setting animation duration to 0
//
// ReduceMotionDurationZero.swift
// Hamburger to Close
//
import SwiftUI
struct ReduceMotionDurationZero: View {
@State private var isRotating = false
@State private var isHidden = false
// Reduce Motion On
let durationZero = Animation.snappy(duration: 0)
// Reduce Motion OFF
let bouncyFeel = Animation.bouncy(duration: 0.4, extraBounce: 0.2)
// Detect and respond to reduce motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
VStack(spacing: 14){
Rectangle() // top
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? 48 : 0), anchor: .leading)
Rectangle() // middle
.frame(width: 64, height: 10)
.cornerRadius(4)
.scaleEffect(isHidden ? 0 : 1, anchor: isHidden ? .trailing: .leading)
.opacity(isHidden ? 0 : 1)
Rectangle() // bottom
.frame(width: 64, height: 10)
.cornerRadius(4)
.rotationEffect(.degrees(isRotating ? -48 : 0), anchor: .leading)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.accessibilityLabel("Menu and close icon transition")
.onTapGesture {
withAnimation(reduceMotion ? durationZero : bouncyFeel) {
isRotating.toggle()
isHidden.toggle()
}
}
}
}
#Preview {
ReduceMotionDurationZero()
.preferredColorScheme(.dark)
}
- Test animations: Ask Siri to “Turn on VoiceOver.”
- Hide decorative decorative animations
- Navigate with a swipe gesture
- VoiceOver skips the animation
Circle()
.trim(from: starting ? 1/3 : 1/9, to: ending ? 2/5 : 1)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round))
.frame(width: 50, height: 50)
.rotationEffect(.degrees(rotating ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotating.toggle()
}
withAnimation(.easeOut(duration: 1).delay(0.5).repeatForever(autoreverses: true)) {
starting.toggle()
}
}
Checkout the sound version on Vimeo
- Add labels for animations that have meaning
Circle()
.trim(from: starting ? 1/3 : 1/9, to: ending ? 2/5 : 1)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round))
.frame(width: 50, height: 50)
.rotationEffect(.degrees(rotating ? 360 : 0))
.accessibilityLabel("Loading Animation")
//.accessibilityAddTraits()
.accessibilityValue("Animation")
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotating.toggle()
}
withAnimation(.easeOut(duration: 1).delay(0.5).repeatForever(autoreverses: true)) {
starting.toggle()
}
}
Checkout the sound version on Vimeo
- Mimicking physical touch and drag:
- Example: Stitch
-
Silent Mode On: Emulate the absence of sound
-
Example: Reporting an incoming or outgoing call
- Screen flashing can cause headaches and seizure
- Provide similar visual effects without requiring motion
- Excessive motion can cause discomfort, dizziness
- Example: Parallax, sliding animations
- Be mindful of motion usage
- Prefer using NavigationLink: Avoid custom slide transitions
- Reduce Motion: Provide options to limit ****animation and motion effects
- Is VoiceOver enabled? Think of how you can clearly translate animations
- Spent time on what can be dizzying/jarring
- Use subtle motion effects
- Can this animation cause discomfort?
- How can people with motion sensitivities enjoy my app?
- What if the user’s reduced motion setting is on?
- Will springiness/bounciness feel out of place?
- Apple Design Awards: Inclusivity Winners
- Apple Human Interface: Accessibility
- Accessibility: Apple Developer Videos
- Responsive Design For Motion
- Playing Haptics
- Motion Sickness
- What Parallax Lacks
- Why iOS 7 Making Some People Sick
- Your Phone Will Never Throw You Up Anymore
- Apple Accessibility: YouTube
- Motion Sensitivity
- Play, Pause, Hide
- Vestibular Disorder