emilkowal-animations-swift
Installation
SKILL.md
SwiftUI & AppKit Animation Best Practices
Comprehensive animation guide for Apple platform interfaces, adapted from Emil Kowalski's web animation principles and Framer Motion best practices. Contains 86 rules across 10 categories targeting iOS 17+, prioritized by impact.
Review Format (Required)
When reviewing UI animation code, you MUST use a markdown table with Before/After/Why columns. Do NOT use a list with "Before:" and "After:" on separate lines. Always output an actual markdown table like this:
| Before | After | Why |
|---|---|---|
.animation(.default) |
.animation(.easeOut(duration: 0.2)) |
Specify explicit easing; avoid default |
scaleEffect(0) |
scaleEffect(0.95) with .opacity(0) |
Nothing in the real world appears from nothing |
.easeIn on dropdown |
.easeOut(duration: 0.2) |
easeIn feels sluggish; easeOut gives instant feedback |
| No press feedback on button | scaleEffect(isPressed ? 0.97 : 1.0) |
Buttons must feel responsive to press |
.scaleEffect(anchor: .center) on popover |
.scaleEffect(anchor: .top) matching trigger |
Popovers should scale from their trigger, not center |
One row per issue found. The "Why" column briefly explains the reasoning.
Review Checklist
When reviewing SwiftUI animation code, check for these issues:
| Issue | Fix |
|---|---|
.animation(.default) or no explicit curve |
Specify exact easing: .easeOut(duration: 0.2) or .spring(duration: 0.3) |
scaleEffect(0) entry animation |
Start from scaleEffect(0.95) with .opacity(0) |
.easeIn on entering UI element |
Switch to .easeOut or .spring() |
.scaleEffect(anchor: .center) on popover |
Set anchor to match trigger location (modals are exempt) |
| Animation on keyboard-initiated action | Remove animation entirely — use instant state change |
| Duration > 300ms on UI element | Reduce to 150-250ms or use spring |
Hover animation without #if os(macOS) guard |
Gate behind platform check or .onHover availability |
withAnimation wrapping high-frequency action |
Remove — actions triggered 100+/day should be instant |
Animating .frame() or .padding() |
Use .scaleEffect, .offset, .opacity instead (GPU-accelerated) |
| Same enter/exit transition speed | Make exit faster: .asymmetric(insertion: 0.3s, removal: 0.15s) |
| Elements all appear at once | Add stagger delay (30-80ms between items) |
Manual Timer/CADisplayLink animation loop |
Use .animation() or withAnimation (runs on render server) |
Missing accessibilityReduceMotion check |
Add @Environment(\.accessibilityReduceMotion) guard |
| No press feedback on tappable element | Add scaleEffect(0.97) on press via ButtonStyle |
When to Apply
Reference these guidelines when:
- Adding animations to SwiftUI views
- Choosing easing curves, springs, or timing values
- Implementing gesture-based interactions (drag, tap, long press)
- Building transitions and navigation animations
- Using
matchedGeometryEffectfor shared element transitions - Creating scroll-linked or parallax effects
- Using iOS 17+
PhaseAnimatororKeyframeAnimator - Optimizing animation performance
- Ensuring animation accessibility with
accessibilityReduceMotion - Writing AppKit/macOS-specific animations
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Timing Curves & Easing | CRITICAL | ease- |
| 2 | Duration & Timing | CRITICAL | timing- |
| 3 | Animation Properties | HIGH | props- |
| 4 | Transforms & Effects | HIGH | transform- |
| 5 | Gesture & Interaction | HIGH | gesture- |
| 6 | Transitions & Navigation | MEDIUM-HIGH | transition- |
| 7 | Scroll & Parallax | MEDIUM | scroll- |
| 8 | Strategic Animation | MEDIUM | strategy- |
| 9 | Accessibility & Polish | MEDIUM | polish- |
| 10 | AppKit Specific | LOW-MEDIUM | appkit- |
Quick Reference
1. Timing Curves & Easing (CRITICAL)
ease-prefer-easeout- Use easeOut as your default easingease-avoid-linear- Avoid linear easing for UI animationsease-spring-default- Prefer springs for natural motionease-spring-ios17- Use iOS 17 Spring struct with bounce/durationease-custom-timing-curve- Create custom curves with timingCurveease-ease-in-for-exits- Use easeIn for exit animationsease-ease-inout-for-emphasis- Use easeInOut for emphasis motionease-spring-response- Configure spring response for duration feelease-spring-damping- Configure spring damping for bounceease-match-context- Match easing to animation context
2. Duration & Timing (CRITICAL)
timing-200ms-default- Use 200ms as default UI animation durationtiming-100ms-micro- Use 100ms for micro-interactionstiming-300ms-emphasis- Keep emphasis animations under 300mstiming-stagger-children- Stagger child animations for orchestrationtiming-delay-strategic- Use delay strategically for sequencing
3. Animation Properties (HIGH)
props-opacity-scale- Prefer opacity and scale for performanceprops-avoid-size-animate- Avoid animating frame size directlyprops-transform-origin- Set anchor points for transformsprops-drawing-group- Use drawingGroup for complex hierarchiesprops-animation-disable- Use animation(nil) to prevent animationsprops-explicit-animation- Use withAnimation for explicit controlprops-implicit-animation- Use .animation modifier for implicit animations
4. Transforms & Effects (HIGH)
transform-scale-subtle- Use subtle scale values (0.95-0.98)transform-rotation-purposeful- Apply rotation with purposetransform-translate-direction- Translate in meaningful directionstransform-3d-perspective- Use rotation3DEffect for depthtransform-order-matters- Modifier order affects outputtransform-anchor-point- Set anchor for rotation/scale origintransform-combine-effects- Combine transforms purposefullytransform-blur-crossfade- Use blur to mask imperfect crossfades
5. Gesture & Interaction (HIGH)
gesture-tap-feedback- Provide immediate tap feedbackgesture-long-press- Animate long press statesgesture-drag-basic- Implement smooth drag interactionsgesture-drag-constraints- Constrain drag within boundsgesture-drag-velocity- Use velocity for momentum effectsgesture-gesture-state- Use @GestureState for transient valuesgesture-updating-modifier- Use .updating for live feedbackgesture-simultaneous-vs-exclusive- Choose gesture compositiongesture-magnify-gesture- Implement pinch-to-zoomgesture-rotation-gesture- Implement rotation gesturesgesture-hover-macos- Handle hover on macOSgesture-sensory-feedback- Pair gestures with hapticsgesture-spring-on-release- Spring back on gesture endgesture-cancellation- Handle gesture cancellation gracefullygesture-asymmetric-press-release- Asymmetric press/release timing
6. Transitions & Navigation (MEDIUM-HIGH)
transition-builtin-types- Use built-in transition typestransition-asymmetric- Use asymmetric for different enter/exittransition-combined- Combine transitions for richer effectstransition-custom-modifier- Create custom transition modifierstransition-matched-geometry- Use matchedGeometryEffect properlytransition-namespace-scope- Manage @Namespace lifecycletransition-navigation-transitions- Customize navigation transitionstransition-sheet-presentations- Animate sheet presentationstransition-id-for-replacement- Use .id() for view replacementtransition-content-transition- Use contentTransition for text
7. Scroll & Parallax (MEDIUM)
scroll-geometry-reader- Use GeometryReader for scroll positionscroll-preference-key- Use PreferenceKey for scroll datascroll-parallax-effect- Create parallax scroll effectsscroll-sticky-header- Implement animated sticky headersscroll-scroll-transition- Use iOS 17 scrollTransition modifierscroll-visual-effect- Use iOS 17 visualEffect modifier
8. Strategic Animation (MEDIUM)
strategy-purposeful-motion- Every animation needs purposestrategy-hierarchy-emphasis- Use motion to show hierarchystrategy-state-communication- Animate state changes clearlystrategy-spatial-continuity- Maintain spatial relationshipsstrategy-brand-expression- Express brand through motionstrategy-frequency-framework- Decide by usage frequency before animatingstrategy-no-keyboard-animation- Never animate keyboard-initiated actionsstrategy-component-design- Component design principles for loved libraries
9. Accessibility & Polish (MEDIUM)
polish-reduce-motion- Respect accessibilityReduceMotionpolish-phase-animator- Use PhaseAnimator for sequencespolish-keyframe-animator- Use KeyframeAnimator for complex pathspolish-animation-completions- Handle animation completionpolish-interruptible-animations- Make animations interruptiblepolish-animation-debugging- Debug animations effectivelypolish-performance-profiling- Profile animation performancepolish-symbol-effects- Use SF Symbol effectspolish-text-animations- Animate text with contentTransitionpolish-haptic-pairing- Pair animations with haptic feedbackpolish-skip-subsequent-delays- Skip delay on subsequent tooltips/popovers
10. AppKit Specific (LOW-MEDIUM)
appkit-nsanimation-context- Use NSAnimationContext for groupingappkit-core-animation- Use Core Animation layersappkit-layer-backed-views- Enable layer backing for performanceappkit-implicit-animations- Leverage implicit layer animationsappkit-spring-animation- Create spring animations in AppKitappkit-animator-proxy- Use animator proxy for view animations
Key Values Reference
| Value | Usage |
|---|---|
.spring(duration: 0.3, bounce: 0.2) |
Standard iOS 17 spring animation |
.spring(response: 0.3, dampingFraction: 0.7) |
Classic spring configuration |
.easeOut(duration: 0.2) |
Standard UI transition |
scaleEffect(0.97) |
Button press feedback |
scaleEffect(0.95) |
Minimum enter scale (never scale to 0) |
0.2 seconds |
Default micro-interaction duration |
0.3 seconds |
Maximum duration for UI animations |
0.5 seconds |
Sheet/drawer animation duration |
Concept Mapping (Web to SwiftUI)
| Web Concept | SwiftUI Equivalent |
|---|---|
ease-out |
.easeOut or Animation.easeOut(duration:) |
cubic-bezier(a,b,c,d) |
.timingCurve(a, b, c, d, duration:) |
| Framer Motion spring | .spring(duration:bounce:) (iOS 17+) |
transform: scale(0.97) |
.scaleEffect(0.97) |
transform-origin |
.scaleEffect(anchor: .topLeading) |
useMotionValue |
@State / @GestureState |
AnimatePresence |
.transition() + .id() |
layoutId |
.matchedGeometryEffect(id:in:) |
useScroll |
GeometryReader + PreferenceKey |
whileHover/whileTap |
.onHover {} / gesture modifiers |
dragConstraints |
DragGesture with onChange bounds |
prefers-reduced-motion |
@Environment(\.accessibilityReduceMotion) |
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |