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.springThe default for interactive elements. Energetic exit, settled arrival.
cubic-bezier(0.16, 1, 0.3, 1).spring(response: 0.4, dampingFraction: 0.8)Spring Bouncy
motion.easing.springBouncyFor playful micro-interactions. Overshoots briefly before settling.
cubic-bezier(0.34, 1.56, 0.64, 1).spring(response: 0.4, dampingFraction: 0.6)Ease Out
motion.easing.easeOutElements entering from off-screen. Fast start, slow settle.
cubic-bezier(0, 0, 0.2, 1).easeOut(duration: 0.25)Ease In
motion.easing.easeInElements leaving the screen. Builds speed before exit.
cubic-bezier(0.4, 0, 1, 1).easeIn(duration: 0.2)Ease In Out
motion.easing.easeInOutCross-fades and morph transitions. Symmetric, balanced.
cubic-bezier(0.4, 0, 0.2, 1).easeInOut(duration: 0.3)Linear
motion.easing.linearLooping animations only — spinners, progress, continuous motion.
linear.linear(duration: 0.15)Duration Scale
Use the shortest duration that still communicates the transition clearly. Never use duration to mask performance problems.
| Name | Value | Typical Use |
|---|---|---|
| Instant | 80ms | Focus rings, active states |
| Fast | 150ms | Hover effects, micro-interactions |
| Normal | 250ms | Panel reveals, component transitions |
| Slow | 400ms | Page transitions, modal open/close |
| Slower | 600ms | Onboarding, 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.
| Token | Value | Notes |
|---|---|---|
--motion-sheet-entry-duration | 0.25s | Total rise time. Matches the Normal duration step. |
--motion-sheet-entry-easing | cubic-bezier(0.16, 1, 0.3, 1) | Spring-out easing — fast exit, slow settle. Same as the Spring curve. |
--motion-sheet-entry-from | 0.97 | Initial scale factor. The sheet grows from 97% → 100% as it rises. |
@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;
}// 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
| Context | Direction |
|---|---|
| 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.
/* 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; }
}
}@Environment(.accessibilityReduceMotion)
var reduceMotion
var sheetAnimation: Animation {
reduceMotion
? .linear(duration: 0.15)
: .spring(response: 0.25,
dampingFraction: 0.8)
}