|

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:

PathWhat It’s ForWhen You’d Use It
SwiftUIMenus, buttons, 2D controlsBuilding control panels, UI overlays, layouts
SwiftUI + RealityViewMixed 2D + 3DMicro interactions
RealityKit EntitiesSpatial 3D contentCharacter animation, spatial storytelling
UIKitLegacy iPadOS appsPorting existing UIKit code
Metal / CompositorServicesCustom renderingGame engines, fully immersive experiences

Detailed breakdown:

Rendering PathWhat AnimatesAnimation MechanismsTiming & ControlWhen to Use
SwiftUI Windows & PanelsWindows, panels, SwiftUI controls, 2D UISwiftUI animation transactions → Core AnimationDeclarative transactionsUI state, menus, tool palettes
Hybrid SwiftUI + RealityViewMixed 2D/3D interfaces, RealityView contentSwiftUI animations + RealityKit clips/systems + visionOS 26 bridgeHybrid timing: SwiftUI transactions plus RealityKit update loopLayered interfaces where 2D controls interact with 3D content
RealityKit EntitiesRealityKit entities driven by state changesEntity.animate, content.animate, ECS systems, AnimationPlaybackControllerFrame-driven, scriptable controlSpatial storytelling, dynamic scenes, entity choreography
UIKit CompatibilityLegacy UIKit apps and layersUIView.animate, CABasicAnimation, CAKeyframeAnimationImplicit/explicit Core AnimationExisting UIKit code bases brought into visionOS
Custom Metal / CompositorServicesCustom visualizations, simulations, bespoke render passesManual vertex/fragment shader animation, compute passes, engine-driven timelinesYou drive time yourself—LayerRenderer.Clock().wait(until:) syncs to displayGames, 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:

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 via AnimationPlaybackController.
Timeline animation

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 AnimationPlaybackController for 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, PhysicsMotionComponent supported 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 withAnimation or rely on implicit transactions for a11y-friendly transitions.
SwiftUI rotate animation
Flip animation
Text animations for messages
Cipher animation

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.
Logo to table animation
Logo animation

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 ShaderGraphMaterial parameters or bindableValues updates.
Glitch text animation

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: ParticleEmitterComponent with tunable birth, lifetime, and behaviour parameters.
Particle animation

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 seconds

Controller 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.7

Event-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:

  1. Duplicate SwiftUI’s curve math in RealityKit—calculate damping by hand and drive it every frame in a custom system.
  2. 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.animate is not a clip player—it only animates property mutations you write in the body closure, using SwiftUI timing. For skeletal or authored motion, use AnimationResource + AnimationPlaybackController.

When to Use Which

  • Use Entity.animate for local, event- or gesture-scoped tweens.
  • Use content.animate if your animation is a side effect of SwiftUI state changes inside RealityView.update.
  • Use AnimationPlaybackController for 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:

  1. Curve duplication — SwiftUI’s spring math drives RealityKit entities directly.
  2. State synchronization — No more threading transforms through RealityView.update manually.
  3. Gesture reactivity — Call Entity.animate from gesture handlers without per-frame systems.
  4. Event observation — No need to watch AnimationEvents.PlaybackCompleted for 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

Questions or feedback?