Spring-based animation rules for a professional production tool. Every transition serves the operator — physics-correct, interruptible, and respectful of accessibility preferences.
v2 — Spring-based · Apple HIG aligned
01 — Principles
01
Physics, not curves
All animations use SwiftUI springs. Springs preserve velocity when interrupted — Bézier curves don't. The interface never jerks.
02
Always interruptible
Every animation can be reversed mid-flight. If the operator opens then immediately closes a panel, the spring catches the velocity and reverses smoothly.
03
One axis, one property
Elements enter on one axis with one transform. Y-translate + opacity. Scale + opacity. Never diagonal, never rotational, never theatrical.
04
Functional only
Animation communicates state change — what appeared, what moved, what completed. If it doesn't answer a question, remove it.
05
Reduce Motion first
Every animation has a cross-fade fallback. When the system Reduce Motion setting is on, slides become fades, scales become fades, durations shorten.
06
Faster than thought
Perceptual duration stays under 350ms. The operator should never wait for the interface to finish animating.
02 — Spring curves
Two SwiftUI spring presets cover the entire app. No custom springs, no Bézier timing curves, no bouncy presets. Alveon uses only critically-damped and slightly-underdamped springs — professional tools don't bounce.
.smooth
duration: 0.5 · bounce: 0 · critically damped
The default for everything. Panels, content transitions, stage changes, modals. Settles without overshoot — direct and precise.
default
.snappy
duration: 0.5 · bounce: 0 · higher stiffness
For fast micro-interactions. Hovers, button presses, toggles, tooltip appear/dismiss. Reaches target faster than .smooth.
fast interactions
.bouncy
Not used in Alveon Studio
Bounce implies playfulness. Alveon is a precision production tool — every millimeter matters. Overshoot on a measurement readout or a slider value would undermine trust.
excluded
Why springs — velocity preservation
Drag the ball and release. The spring picks up whatever velocity you gave it. A Bézier curve ignores your gesture velocity and always plays the same fixed motion.
Spring (velocity preserved)
Bézier (velocity ignored)
Click a ball to animate it to the other side
// SwiftUI Spring Tokens — Alveon StudioextensionAnimation {
// Default: panels, content, stage transitions, modalsstatic let alveon = Animation.smooth(duration: 0.22)
// Fast: hover, press, toggle, tooltipstatic let alveonFast = Animation.snappy(duration: 0.15)
// Emphasis: modals, stage changes, full transitionsstatic let alveonEmphasis = Animation.smooth(duration: 0.35)
}
// UsagewithAnimation(.alveon) {
showInspector = true
}
// Interruptible by default — just call again:withAnimation(.alveon) {
showInspector = false// reverses mid-flight, preserving velocity
}
03 — Duration tokens
Springs don't have a fixed duration — they settle naturally. These are the perceptual durations passed to the spring constructor. The actual settling time is slightly longer, but imperceptible.
80
Instant
Hover states, active presses, checkbox ticks, color changes
150
Quick
Tooltips, dropdowns, small state changes, toggle switches
This will send files to Printer #3 and Knitter #1.
// ShowwithAnimation(.alveonEmphasis) { showModal = true }
// scale(0.96)→1, opacity 0→1, backdrop fades in// DismisswithAnimation(.alveonFast) { showModal = false }
// Exits are always fast — operator made a decision
06 — Reduce Motion
When the macOS "Reduce motion" accessibility setting is enabled, all spatial animations (slide, scale) collapse to simple cross-fades. This is not optional — it's a requirement.
Standard
Inspector panel slides in
Toast slides down
Modal scales up
Reduce Motion
Inspector cross-fades in
Toast cross-fades in
Modal cross-fades in
// Reduce Motion helperstructAlveonTransition {
static funcslideUp() -> AnyTransition {
ifUIAccessibility.isReduceMotionEnabled {
return .opacity
}
return .move(edge: .bottom)
.combined(with: .opacity)
}
static funcscale() -> AnyTransition {
ifUIAccessibility.isReduceMotionEnabled {
return .opacity
}
return .scale(scale: 0.96)
.combined(with: .opacity)
}
}
// Or use the environment value in SwiftUI
@Environment(\.accessibilityReduceMotion) var reduceMotion
07 — Quick reference
Element
Pattern
Spring
Duration
Reduce Motion
Hover state
Background / border color
.snappy
80ms
Keep as-is
Button press
scale(0.98) + opacity
.snappy
80ms
Opacity only
Tooltip
Fade in
.snappy
150ms
Keep as-is
Dropdown
Fade + slideUp(4px)
.snappy
150ms
Fade only
Toast
SlideDown(8px) + fade
.smooth
220ms
Fade only
Inspector panel
Width expand + content fade
.smooth
220ms
Fade only
Player list
Stagger items (40ms)
.smooth
220ms each
Fade, no stagger
Stage change
Dot color + bar width
.smooth
350ms
Keep as-is (color only)
Modal dialog
Scale(0.96→1) + backdrop
.smooth
350ms / 150ms
Fade only
View transition
Cross-fade
.smooth
220ms
Keep as-is
Scan line pulse
Opacity 0.06→0.2→0.06
.smooth
350ms
Keep as-is
Progress bar
Width change
.smooth
350ms
Keep as-is
Sidebar expand
Width 56→200px + fade
.smooth
220ms
Fade only
Connection bar
SlideDown from top
.smooth
220ms
Fade only
08 — Hard rules
Do
Use .smooth as the default spring
Use .snappy for micro-interactions
Keep perceptual durations ≤ 350ms
Stagger lists at 40ms intervals
Animate opacity + one transform only
Make exits faster than entrances
Fade content in after its container
Check @Environment(\.accessibilityReduceMotion)
Provide cross-fade fallback for all spatial motion