Mobile App UI Micro-Animations
A button without visual feedback on tap feels broken. Not "ugly"—broken. User taps again, app processes double tap. Micro-animations are not decoration, they are part of functional UI feedback.
What is Micro-Animation and Where It Lives
Micro-animation is single UI element state change lasting 80–300ms. Button on tap, toggle icon on activation, counter on increment, loading indicator. Everything that reacts to user action or data change.
Wrong to use one standard tap for all buttons. Spring animation with dampingRatio: 0.6 fits destructive actions (delete button), but looks strange on "Next" button in onboarding.
Implementation on iOS: From UIKit to SwiftUI
UIKit: scale + haptics = Proper Tap
Minimal feedback implementation on UIButton:
class SpringButton: UIButton {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
UIView.animate(
withDuration: 0.12,
delay: 0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 8,
options: [.allowUserInteraction]
) {
self.transform = CGAffineTransform(scaleX: 0.94, y: 0.94)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 0.45,
initialSpringVelocity: 6,
options: [.allowUserInteraction]
) {
self.transform = .identity
}
}
}
.allowUserInteraction is mandatory. Without it, button does not respond to taps while animating, creating "sticking" feeling.
SwiftUI: Symbol Animations and Keyframe
SF Symbols 5 (iOS 17+) provide symbolEffect(_:)—ready animations for system icons:
Image(systemName: "heart.fill")
.symbolEffect(.bounce, value: isLiked)
.symbolEffect(.variableColor.iterative.reversing, isActive: isLoading)
.bounce bounces once on isLiked change. No need to write animation manually.
For complex multi-step micro-animations—KeyframeAnimator:
KeyframeAnimator(initialValue: CheckmarkState()) { value in
Circle()
.scaleEffect(value.scale)
.opacity(value.opacity)
} keyframes: { _ in
KeyframeTrack(\.scale) {
LinearKeyframe(0.0, duration: 0.05)
SpringKeyframe(1.2, duration: 0.2, spring: .bouncy)
SpringKeyframe(1.0, duration: 0.15)
}
KeyframeTrack(\.opacity) {
LinearKeyframe(0.0, duration: 0.05)
LinearKeyframe(1.0, duration: 0.1)
}
}
This is pattern for success checkmark animation—scale+opacity simultaneously, with different curves.
Android: Jetpack Compose and AnimatedVisibility
In Compose micro-animations built on animateFloatAsState, animateColorAsState and updateTransition:
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.93f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "button_scale"
)
Box(modifier = Modifier
.scale(scale)
.clickable(
interactionSource = interactionSource,
indication = null // remove ripple, replace with spring
) { onClick() }
)
indication = null removes standard ripple. This is debatable: Material Design 3 assumes ripple as standard. Remove it only where there is proper replacement.
AnimatedVisibility with custom enter/exit for element appearance:
AnimatedVisibility(
visible = showBadge,
enter = scaleIn(
animationSpec = spring(Spring.DampingRatioLowBouncy),
transformOrigin = TransformOrigin(1f, 0f)
) + fadeIn(),
exit = scaleOut(transformOrigin = TransformOrigin(1f, 0f)) + fadeOut()
) {
Badge { Text(count.toString()) }
}
TransformOrigin(1f, 0f) animates from top-right corner, like notification badges.
Lottie for Icons with States
When designer wants complex icon—like, favorite, custom-styled toggle—Lottie is simpler than coding. Animator exports JSON via Bodymovin from After Effects, integrate via lottie-ios or lottie-android.
Lottie issue: files can be heavy. 200 KB JSON for favorite icon—too much. Optimize via lottie-web's dotLottie format (.lottie—ZIP archive), or trim extra layers in editor.
Common Mistakes
Animate backgroundColor directly via UIView—this is Core Animation CATransaction without GPU acceleration for color. Right: change via UIView.animate or use CABasicAnimation on layer backgroundColor.
Forget about reduceMotion. UIAccessibility.isReduceMotionEnabled on iOS, Settings.Global.TRANSITION_ANIMATION_SCALE on Android. Users with vestibular disorders disable animations. Respect this:
if UIAccessibility.isReduceMotionEnabled {
// Simple fade instead of scale+bounce
} else {
// Full animation
}
Process
Collect UI inventory: all interactive elements in app. Agree with designer: which elements animate, what feelings they should convey (confidence, ease, urgency). Develop animation library—reusable components for each type. Haptic feedback where appropriate. Check for reduceMotion.
Timeline
Library of micro-animations for 10–15 interface elements—2–3 business days. If animation via Lottie from scratch (JSON creation by animator) is needed—this is separate stage.







