Deep Dive into Animation on visionOS
You tap a control. You can’t see anything happening where you’re looking, yet a panel slides three meters to the opposite side. If the system doesn’t let you know, you’ll miss it. But you will feel uneasy if it takes up the whole space to show you. Spatial apps live between unnoticed and uncomfortable. One easy fix is animation, which is just intentional change over time to inform users what changed, where it came from, and how it settles.
This article uses the word “animation” to mean visual motion.
Choose Your Path
If you’re wondering “Why animate at all?” or aren’t sure that temporal cues are important for spatial clarity and comfort. Begin with Why Animate? Temporal Cues for Spatial Perception to learn about the perceptual foundations.
If you’re ready to animate and want to know, “What tools exist on visionOS?” You know the “why” but require the practical “what” and “how.” The tool map and implementation guidelines can be found at The Stack: How Animation Responsibilities Split.
By the end, you’ll understand which tool to use for whatever task, whether you’re mixing character motion, adding a spring to a button, or creating cinematic beats. You’ll also have working patterns for clip control, stacking, handoff, and the RealityKit–SwiftUI bridge that was added in visionOS 26.
Why Animate? Temporal Cues for Spatial Perception
Abrupt state jumps—visual, auditory, or spatial—are easy to miss or uncomfortable. Subtle, intentional temporal cues make change detectable, attributable, continuous, and comfortable. Visual motion, as a form of animation, is one channel among several for providing temporal information in spatial perception.
What temporal cues actually solve:
- Detection: “Something changed.” (motion onset, earcon, haptic tick)
- Attribution: “This change came from that thing.” (object moves, camera doesn’t; spatialized audio at the source)
- Continuity: “It’s the same thing, now somewhere else.” (object constancy; fade → expand → settle; skeletal clip blends)
- Comfort: “My senses agree.” (object-centric motion; avoid global camera jumps)
- Latency Masking: “The system is working.” (progressive reveal, loopable earcon)
Apple’s Human Interface Guidelines say the same: favor object motion, honor Reduce Motion, and avoid vestibular conflict.
Working rule: If a motion doesn’t earn detection, attribution, continuity, or comfort—delete it. Clarity and comfort should take precedence over delight in design.
Now that you know why you should animate, the following question is where the motion exists. On visionOS, animation is distributed across numerous rendering pathways, each with its own time, rules, and goals. Understanding this distinction enables you to choose the appropriate tool and stop wrestling with the framework.
The Landscape: Three Questions Shape Your Choice
Before we list renderers and APIs, let’s use three questions to plot the landscape. Answering them turns a wall of terminology into a trail you can follow.
Question 1: Who authors the motion?
- Designer-authored clips — A motion designer exports timelines (Blender, Maya, Reality Composer Pro) and you trigger them at runtime.
- Runtime choreography — You or your code generate motion on the fly, reacting to state, events, gestures, or simulation.
Question 2: What needs to move?
- Windows and panels — SwiftUI layers that live in the 2.5D windowing world.
- Spatial entities — RealityKit entities that live in the 3D scene.
- Both together — Mixed compositions that share timing between SwiftUI and RealityKit.
Question 3: How much complexity is justified?
- Micro interactions — A quick scale, fade, or translate to clarify visual state.
- Choreographed performances — Layered clips, locomotion blends, or storytelling beats that demand precise control.
- Physically responsive motion — Simulations, particles, or physics components that trade determinism for realism.
Keep those three questions in mind. The rest of this guide shows how every rendering method and mechanism relates back to them, so you’ll always know why a tool is right for your project.
The Stack: How Animation Responsibilities Split
In visionOS, there are five rendering paths, each with unique animation capabilities and limitations. Four paths flow through the Render Server (which contains Core Animation and RealityKit rendering as parallel subsystems), and one bypasses it entirely.
Quick reference:
| Path | What It’s For | When You’d Use It |
|---|---|---|
| SwiftUI | Menus, buttons, 2D controls | Building control panels, UI overlays, layouts |
| SwiftUI + RealityView | Mixed 2D + 3D | Micro interactions |
| RealityKit Entities | Spatial 3D content | Character animation, spatial storytelling |
| UIKit | Legacy iPadOS apps | Porting existing UIKit code |
| Metal / CompositorServices | Custom rendering | Game engines, fully immersive experiences |
Detailed breakdown:
| Rendering Path | What Animates | Animation Mechanisms | Timing & Control | When to Use |
|---|---|---|---|---|
| SwiftUI Windows & Panels | Windows, panels, SwiftUI controls, 2D UI | SwiftUI animation transactions → Core Animation | Declarative transactions | UI state, menus, tool palettes |
| Hybrid SwiftUI + RealityView | Mixed 2D/3D interfaces, RealityView content | SwiftUI animations + RealityKit clips/systems + visionOS 26 bridge | Hybrid timing: SwiftUI transactions plus RealityKit update loop | Layered interfaces where 2D controls interact with 3D content |
| RealityKit Entities | RealityKit entities driven by state changes | Entity.animate, content.animate, ECS systems, AnimationPlaybackController | Frame-driven, scriptable control | Spatial storytelling, dynamic scenes, entity choreography |
| UIKit Compatibility | Legacy UIKit apps and layers | UIView.animate, CABasicAnimation, CAKeyframeAnimation | Implicit/explicit Core Animation | Existing UIKit code bases brought into visionOS |
| Custom Metal / CompositorServices | Custom visualizations, simulations, bespoke render passes | Manual vertex/fragment shader animation, compute passes, engine-driven timelines | You drive time yourself—LayerRenderer.Clock().wait(until:) syncs to display | Games, fully immersive experiences, effects that bypass Render Server |
Key insight: The first four paths use the Render Server (where Core Animation and RealityKit rendering work in tandem), then merged in the Compositor. The fifth path (CompositorServices) bypasses the Render Server and submits frames directly to the Compositor.

See also: Understanding the visionOS render pipeline
Finding Your Tool: From Questions to Mechanisms
The answer to the three landscape questions—who writes the motion, what moves, and how complex—are key to connect to the 10 animation mechanisms available in visionOS 26. Use the decision points below to easily choose the best one.
Quick navigation:
- Designer hands you clips → Reality Composer Pro, Skeletal Clips
- You generate motion at runtime → Entity Actions, ECS Systems, Physics
- UI needs to move → SwiftUI Animation, visionOS 26 Bridge
- Visual effects → Material Parameters, Particles, Blend Shapes
If a designer hands you motion assets…
You are triggering clips that were authored outside Xcode. Your job is to wire them into the app without losing nuance. For that you would use
1. Reality Composer Pro Timelines (scene-level animation sequences)
- Use for: Complex cinematic sequences, multi-property keyframe animation, designer-owned motion.
- Avoid: Runtime-only logic or simple procedural tweens.
- Key: Export timelines as USD with animation libraries; discover via
.availableAnimations. Designers iterate visually, engineers trigger playback viaAnimationPlaybackController.
2. Skeletal/Blend-Shape Clips (single-asset animation clips)
- Use for: Character locomotion, repeatable motion cycles (walk, run, idle), pre-baked deformation.
- Avoid: Tiny one-off nudges.
- Key: Clips authored in DCC tools (Blender, Maya, etc.), exported as USDZ, then driven through
AnimationPlaybackControllerfor precise scrubbing, blending, and speed control.
If you choreograph animation at runtime…
You create motion on the fly (procedurally), maybe to draw attention to a state change or react to a gesture. Start simple and work your way up to Systems as the complexity goes up.
3. Entity Actions
- Use for: Reusable procedural beats like spins, emphasis pulses, or point-to animations.
- Avoid: Multi-layer choreography or character blending.
- Key: Declarative wrappers (
SpinAction,EmphasizeAction,FromToByAction,OrbitEntityAction) that let you script quick motion without building full systems.
4. Procedural Systems (RealityKit ECS)
- Use for: Reactive behaviours—IK constraints, gaze tracking, steering, dynamic transforms tied to input.
- Avoid: Long-form cinematic sequences (bake those as clips).
- Rule: Maintain one writer per property; keep systems deterministic and performance-aware.
5. Physics Simulation
- Use for: Collisions, gravity, joints, ragdoll motion—when realism from physics outweighs precise choreography.
- Avoid: Tight storytelling beats that require frame-exact control.
- Components:
RigidBodyComponent,PhysicsBodyComponent,PhysicsMotionComponentsupported by RealityKit’s solver.
If your UI world needs to move…
SwiftUI owns windows, panels, and controls. Leverage its declarative timing, then reach for the bridges when 3D entities must follow the same beat.
6. SwiftUI Animation
- Use for: Window and panel transitions, control emphasis, all 2D UI state changes.
- Avoid: Driving 3D entity motion directly (pre-26, it never crossed into RealityKit).
- Key: Honors Reduce Motion automatically; wrap state mutations in
withAnimationor rely on implicit transactions for a11y-friendly transitions.
7. visionOS 26 SwiftUI Bridge (Entity.animate, RealityViewContent.animate)
- Use for: One-off property tweens (scale, position, rotation) on RealityKit entities driven by SwiftUI timing with optional callback when completed.
- Avoid: Skeletal motion or authored clip orchestration.
- Key: Eliminates “microtween friction.” Borrow SwiftUI’s transaction curves without recreating them in RealityKit or exporting throwaway clips.
If you paint with materials (VFX)
Sometimes motion means light, surface, or atmosphere rather than transforms. These mechanisms live on the rendering surface.
8. Material Parameters
- Use for: “Glow” pulses, dissolve wipes, hue/intensity ramps driven through shader parameters.
- Avoid: Unique per-entity materials that explode draw calls; prefer shared materials with animated bindings.
- Key: Animate values via
ShaderGraphMaterialparameters orbindableValuesupdates.
9. Geometry Modifier Shaders
- Use for: Procedural vertex-level VFX—waving, rippling, terrain displacement, noise-driven deformation.
- Avoid: Pre-baked character animation (use blend shapes or skeletal rigs).
- Key: Author geometry modifiers visually in Reality Composer Pro’s Shader Graph editor using MaterialX nodes. Connect a Geometry Modifier node to the Custom Geometry Modifier input of your material’s output node. Animate parameters (like displacement strength, time offsets) via shader graph inputs or bind runtime values from code
10. Particle Emitters
- Use for: Transient atmospherics—impact sparks, trails, smoke, confetti.
- Avoid: Film-scale VFX (RealityKit emitters are CPU-bound and favor simplicity).
- Component:
ParticleEmitterComponentwith tunable birth, lifetime, and behaviour parameters.
The Controller: Clip Playback Backbone
AnimationPlaybackController has been the way to control entity animation since RealityKit’s introduction. It’s imperative (you issue commands), powerful (full playback control), and precise (scrub to milliseconds)—you call entity.playAnimation(...) and get back a controller handle that lets you pause, resume, scrub, adjust speed, and query state.
Think of the controller as your remote; the clip is the movie.
Core API
let controller = entity.playAnimation(runClip, transitionDuration: 0.2, startsPaused: false)
controller.pause()
controller.resume()
controller.stop(blendOutDuration: 0.15)
controller.speed = 1.25
controller.time = 2.0 // scrub to 2 secondsController Properties (Read-Only)
isPlaying— true if currently playing.isPaused— true if paused.isComplete— true if the animation reached the end.isValid— false after animation ends or is stopped.
Transition Control When Starting Animations
When you call entity.playAnimation(...) to start a new clip, you can control how it transitions from the currently playing animation using the transitionDuration parameter. For more complex scenarios, the full signature includes additional parameters:
entity.playAnimation(
_: AnimationResource,
transitionDuration: TimeInterval,
blendLayerOffset: Int32,
separateAnimatedValue: Bool,
startsPaused: Bool,
clock: AnimationClock?,
handoffType: AnimationHandoffType
)Key transition parameters:
transitionDuration— How long to blend from the current animation to the new one.handoffType— How the new animation takes over (specified at creation time, not a controller property):.snapshotAndReplace(applyToAllLayers:)(default): Stop current, snapshot its value as transition starts..replace(applyToAllLayers:): Keep current running during transition..compose: Additive-style composition (e.g., aim offset over locomotion)..stop: Terminate specific layers.blendLayerOffset— Stack animations on different layers for simultaneous playback.separateAnimatedValue— Whether the animation writes to a separate value that doesn’t persist after the animation ends.
Layering
Visualize layers as separate tracks in a music editor, each contributing to the overall composition like different instruments in a symphony. Locomotion runs on layer 0—your character walks. Upper-body aim sits on layer 1—she points while walking. Adjust the controller’s blendFactor property (0.0 to 1.0) to dial the contribution of that layer.
Worked Examples
Let’s put those concepts in motion.
Cross-Fade
let entity = try await Entity(named: "Character", in: .main)
guard let idle = entity.availableAnimations.first(where: { $0.name == "Idle" }),
let run = entity.availableAnimations.first(where: { $0.name == "Run" }) else {
return
}
var controller = entity.playAnimation(idle, transitionDuration: 0.0)
// Later, switch to run with 0.25s cross-fade
controller = entity.playAnimation(
run,
transitionDuration: 0.25,
blendLayerOffset: 0,
separateAnimatedValue: false,
startsPaused: false,
clock: nil,
handoffType: .snapshotAndReplace(applyToAllLayers: true)
)Additive Upper-Body Overlay
let locomotion = entity.playAnimation(run, blendLayerOffset: 0)
let aim = entity.playAnimation(
aimUpperBody,
transitionDuration: 0.15,
blendLayerOffset: 1,
separateAnimatedValue: false,
startsPaused: false,
clock: nil,
handoffType: .compose
)
aim.blendFactor = 0.7Event-Driven Chaining
let token = scene.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: entity) { event in
guard event.playbackController.isValid else { return }
_ = entity.playAnimation(wave, transitionDuration: 0.2)
}visionOS 26: Bridging SwiftUI Timing into RealityKit
AnimationPlaybackController remains the backbone for skeletal animation, layered blends, and clip orchestration. For micro interactions, though, there was friction. Before visionOS 26, even a “simple” spring nudge on a RealityKit entity meant extra plumbing (and even more if you wanted to observe the animation events).
Pre-26 you faced two unsatisfying options:
- Duplicate SwiftUI’s curve math in RealityKit—calculate damping by hand and drive it every frame in a custom system.
- Author a throwaway clip in Reality Composer Pro just for a single property tween.
Both felt heavy for what should have been a handful of lines. visionOS 26 adds a bridge so SwiftUI’s timing animates RealityKit transforms directly.
1. RealityViewContent.animate { ... }
Use inside RealityView.update when animation is a side effect of SwiftUI state changes. It pulls the current SwiftUI animation transaction into RealityKit—like a bridge cable carrying timing curves across the renderer boundary.
struct Rig: View {
@State private var expanded = false
var body: some View {
VStack {
Toggle("Expand", isOn: $expanded)
RealityView { content in
let box = ModelEntity(mesh: .generateBox(size: 0.1))
box.name = "box"
content.add(box)
} update: { content in
content.animate {
guard let box = content.entities.first(where: { $0.name == "box" }) else { return }
var transform = box.transform
transform.scale = expanded ? .init(repeating: 1.8) : .one
box.transform = transform
}
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.7), value: expanded)
}
}2. Entity.animate(_:, body:)
A type method—call it anywhere (gestures, event handlers, systems) to run a block of RealityKit mutations under a SwiftUI Animation. No state plumbing, no update closures. Just motion.
Entity.animate(.easeInOut(duration: 0.35)) {
myEntity.position += [0.0, 0.1, 0.0]
myEntity.orientation *= simd_quatf(angle: .pi / 8, axis: [0, 1, 0])
}
Note:
Entity.animateis not a clip player—it only animates property mutations you write in the body closure, using SwiftUI timing. For skeletal or authored motion, useAnimationResource+AnimationPlaybackController.
When to Use Which
- Use
Entity.animatefor local, event- or gesture-scoped tweens. - Use
content.animateif your animation is a side effect of SwiftUI state changes insideRealityView.update. - Use
AnimationPlaybackControllerfor skeletal clips, multi-layer blending, handoff control, or any authored motion.
The Bridge Pattern: Evolution of Spatial APIs
visionOS 26 continues (the welcomed) Apple’s trend of building bridges to make APIs higher level. SwiftUI already owns declarative animation curves; RealityKit owns entity transforms. Rather than forcing you to duplicate math, the new bridge carries timing across that boundary and you dont need to worry about the internals.
Before visionOS 26—manual spring system
// Per-frame spring update (simplified)
struct SpringSystem: System {
@MainActor var target = Transform()
func update(context: SceneUpdateContext) {
context.scene.performQuery(.all(of: MotionComponent.self)) { entity, motion in
let delta = Float(context.deltaTime)
var state = motion.state
state.velocity += state.spring * (target.translation - state.current) * delta
state.current += state.velocity * delta
entity.transform.translation = state.current
entity.components.set(MotionComponent(state: state))
}
}
}After visionOS 26—bridge carries SwiftUI timing
Entity.animate(.spring(duration: 0.4, bounce: 0.35)) {
entity.position = SIMD3<Float>(0, 0.5, 0)
}With the bridge in place, whole classes of friction points disappear:
- Curve duplication — SwiftUI’s spring math drives RealityKit entities directly.
- State synchronization — No more threading transforms through
RealityView.updatemanually. - Gesture reactivity — Call
Entity.animatefrom gesture handlers without per-frame systems. - Event observation — No need to watch
AnimationEvents.PlaybackCompletedfor simple tweens.
The bridge makes an overlap area where the time and attributes of SwiftUI and RealityKit intersect. It doesn’t take the role of AnimationPlaybackController for skeletal clips or complicated choreography. Instead, it handles the micro-interactions that were awkward before visionOS 26, leaving authored motion and procedural systems to do what they do best.
Prioritization Matrix: Choosing Animation Tools on visionOS 26
How to use this matrix:
- Difficulty of Implementation: From Easy (quick, low overhead) to Hard (complex setup, steep learning curve).
- Visual Fidelity: Quality and subtlety of motion possible.
- Performance Impact: Typical runtime cost.
- Flexibility: Applicability across scenarios and ease of adaptation.
- Accessibility: Whether motion honours Reduce Motion automatically or requires manual alternatives (audio, haptics, copy).
- Primary Tool: The core API or tool recommended.
- Typical Use Case: Common scenarios where the tool excels.
- Notes: Important caveats or implementation tips.
The prioritization matrix guides tool selection based on individual project requirements and limitations, emphasizing tailored solutions over a one-size-fits-all approach.
Accessibility Patterns for Manual Handling
For mechanisms marked “Manual” in the Accessibility column, read the accessibilityReduceMotion environment value and supply alternative timing or sensory cues. Use Pattern 1 when the motion itself is acceptable but needs gentler timing; reach for Pattern 2 when the motion style needs to change entirely.
@Environment(\.accessibilityReduceMotion) private var reduceMotion
// Pattern 1: Alternate timing curves (same motion, different timing)
func emphasizeWithAlternateCurve(_ entity: Entity, emphasized: Bool) {
let animation = reduceMotion
? Animation.easeIn(duration: 0.15) // Reduced: fast, no bounce
: Animation.spring(duration: 0.4, bounce: 0.35) // Full: springy lift
Entity.animate(animation) {
entity.scale = emphasized ? SIMD3(repeating: 1.2) : .one
}
}
// Pattern 2: Substitute motion type (different visual effect)
func emphasizeWithSubstitution(_ entity: Entity, emphasized: Bool) {
if reduceMotion {
entity.opacity = emphasized ? 1.0 : 0.8 // Subtle fade instead of travel
} else {
entity.position += SIMD3<Float>(0, 0.5, 0) // Full spatial motion
}
}Safe substitutions: spin → fade + scale, long travel → short nudge + fade, spring → ease without bounce. Pair motion with spatial audio or haptics when visuals are reduced.
Editors note: I want to express my gratitude to Cristian Díaz for providing this incredible article to the visionOS community. Thank you for stepping up to teach us about animation on visionOS.
– Joseph, Step Into Vision

Follow Step Into Vision