Motion

Motion is not decoration — it communicates state changes, establishes hierarchy, and gives the interface a sense of physicality. Every transition in Sitka is intentional.

Easing Curves

Hit replay on any curve to see it animate. Spring-based curves are preferred for interactive elements — they feel physical, not timed.

Spring

motion.easing.spring

The default for interactive elements. Energetic exit, settled arrival.

CSScubic-bezier(0.16, 1, 0.3, 1)
Swift.spring(response: 0.4, dampingFraction: 0.8)

Spring Bouncy

motion.easing.springBouncy

For playful micro-interactions. Overshoots briefly before settling.

CSScubic-bezier(0.34, 1.56, 0.64, 1)
Swift.spring(response: 0.4, dampingFraction: 0.6)

Ease Out

motion.easing.easeOut

Elements entering from off-screen. Fast start, slow settle.

CSScubic-bezier(0, 0, 0.2, 1)
Swift.easeOut(duration: 0.25)

Ease In

motion.easing.easeIn

Elements leaving the screen. Builds speed before exit.

CSScubic-bezier(0.4, 0, 1, 1)
Swift.easeIn(duration: 0.2)

Ease In Out

motion.easing.easeInOut

Cross-fades and morph transitions. Symmetric, balanced.

CSScubic-bezier(0.4, 0, 0.2, 1)
Swift.easeInOut(duration: 0.3)

Linear

motion.easing.linear

Looping animations only — spinners, progress, continuous motion.

CSSlinear
Swift.linear(duration: 0.15)

Duration Scale

Use the shortest duration that still communicates the transition clearly. Never use duration to mask performance problems.

NameValueTypical Use
Instant80msFocus rings, active states
Fast150msHover effects, micro-interactions
Normal250msPanel reveals, component transitions
Slow400msPage transitions, modal open/close
Slower600msOnboarding, first-load emphasis

Principles

Physics, not timers

Spring animations communicate mass and momentum. They feel alive because they mirror how real objects behave.

Direction matters

Elements entering from below slide up. Drawers enter from their edge. Motion direction should match spatial meaning.

Choreography

Stagger children by 30–50ms. The eye can only track one thing at a time; give it a path to follow.

Panel Rise

Panel Rise is the standard entry animation for sheets, modals, and overlays. The surface scales from 97% → 100% while fading in, giving the impression of a physical panel rising toward the user. The spring-out easing lands crisply without a linear feel.

TokenValueNotes
--motion-sheet-entry-duration0.25sTotal rise time. Matches the Normal duration step.
--motion-sheet-entry-easingcubic-bezier(0.16, 1, 0.3, 1)Spring-out easing — fast exit, slow settle. Same as the Spring curve.
--motion-sheet-entry-from0.97Initial scale factor. The sheet grows from 97% → 100% as it rises.
CSS — @keyframes
@keyframes panel-rise {
  from {
    opacity: 0;
    transform: scale(var(--motion-sheet-entry-from, 0.97));
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.sheet-enter {
  animation:
    panel-rise
    var(--motion-sheet-entry-duration, 0.25s)
    var(--motion-sheet-entry-easing,
        cubic-bezier(0.16, 1, 0.3, 1))
    both;
}
SwiftUI — .transition
// Warren's SheetTransition modifier
extension AnyTransition {
    static var panelRise: AnyTransition {
        .asymmetric(
            insertion: .scale(scale: 0.97)
                .combined(with: .opacity)
                .animation(.spring(
                    response: 0.25,
                    dampingFraction: 0.8
                )),
            removal: .scale(scale: 0.97)
                .combined(with: .opacity)
                .animation(.easeIn(duration: 0.15))
        )
    }
}

Per-context behaviour

ContextDirection
Action sheet / bottom sheet↑ Slide + scale up from bottom edge
Modal / dialog↑ Scale from 0.97 → 1.0, fade in from 0 → 1
Popover / dropdown↓ Scale from 0.96 → 1.0, origin at trigger
Side drawer← Slide in from leading/trailing edge
Toast / notification↑ Slide in from bottom, 30px travel

Reduced motion

Users who have enabled "Reduce Motion" in their OS settings must get a simplified experience. Never remove all feedback — replace motion with an instant opacity cross-fade.

CSS — prefers-reduced-motion
/* Default: spring-based panel rise */
.sheet-enter {
  animation: panel-rise 0.25s
    cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Reduced: instant fade only */
@media (prefers-reduced-motion: reduce) {
  .sheet-enter {
    animation: fade-in 0.15s linear both;
  }

  @keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}
SwiftUI — accessibilityReduceMotion
@Environment(.accessibilityReduceMotion)
var reduceMotion

var sheetAnimation: Animation {
    reduceMotion
        ? .linear(duration: 0.15)
        : .spring(response: 0.25,
                  dampingFraction: 0.8)
}