29. Animations, transitions, and composition

Goal

Why this matters

Prerequisites

1. Keyframe animation building blocks

Avalonia's declarative animation stack lives in Avalonia.Animation.Animation and friends. Every control derives from Animatable, so you can plug animations into styles or run them directly in code.

Concept Type Highlights
Timeline Animation (Animation.cs) Duration, Delay, IterationCount, PlaybackDirection, FillMode, SpeedRatio
Track KeyFrame (KeyFrames.cs) Specifies a cue (0%..100%) with one or more Setters
Interpolation Animator<T> (Animators/DoubleAnimator.cs, etc.) Avalonia ships animators for primitives, transforms, brushes, shadows
Easing Easing (Animation/Easings/*) Over 30 easing curves, plus SplineEasing for custom cubic Bezier
Clock IClock / Clock (Clock.cs) Drives animations, default is the global clock

A minimal style animation:

<Window xmlns="https://github.com/avaloniaui">
  <Window.Styles>
    <Style Selector="Rectangle.alert">
      <Setter Property="Fill" Value="Red"/>
      <Style.Animations>
        <Animation Duration="0:0:0.6"
                   IterationCount="INFINITE"
                   PlaybackDirection="Alternate">
          <KeyFrame Cue="0%">
            <Setter Property="Opacity" Value="0.4"/>
            <Setter Property="RenderTransform.ScaleX" Value="1"/>
            <Setter Property="RenderTransform.ScaleY" Value="1"/>
          </KeyFrame>
          <KeyFrame Cue="100%">
            <Setter Property="Opacity" Value="1"/>
            <Setter Property="RenderTransform.ScaleX" Value="1.05"/>
            <Setter Property="RenderTransform.ScaleY" Value="1.05"/>
          </KeyFrame>
        </Animation>
      </Style.Animations>
    </Style>
  </Window.Styles>
</Window>

Key points:

2. Controlling playback from code

Animation.RunAsync and Animation.Apply let you start, await, or conditionally run animations from code-behind or view models (Animation.cs, RunAsync).

public class ToastController
{
    private readonly Animation _slideIn;
    private readonly Animation _slideOut;
    private readonly Border _host;

    public ToastController(Border host, Animation slideIn, Animation slideOut)
    {
        _host = host;
        _slideIn = slideIn;
        _slideOut = slideOut;
    }

    public async Task ShowAsync(CancellationToken token)
    {
        await _slideIn.RunAsync(_host, token); // awaits completion
        await Task.Delay(TimeSpan.FromSeconds(3), token);
        await _slideOut.RunAsync(_host, token); // reuse the same host, different cues
    }
}

Behind the scenes RunAsync applies the animation with an IClock (defaults to Clock.GlobalClock) and completes when the last animator reports completion. Create the _slideOut animation by cloning _slideIn, switching its cues, or temporarily setting PlaybackDirection = PlaybackDirection.Reverse before calling RunAsync.

Reactive triggers map easily to animations by using Apply(control, clock, IObservable<bool> match, Action onComplete):

var animation = (Animation)Resources["HighlightAnimation"];
var match = viewModel.WhenAnyValue(vm => vm.IsDirty);
var subscription = animation.Apply(border, null, match, null);
_disposables.Add(subscription);

3. Implicit transitions and styling triggers

For property tweaks (hover states, theme switches) Animatable.Transitions (Animatable.cs) is lighter weight than keyframes. A Transition<T> blends from the old value to a new one automatically.

<Button Classes="primary">
  <Button.Transitions>
    <Transitions>
      <DoubleTransition Property="Opacity" Duration="0:0:0.150"/>
      <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.200"/>
    </Transitions>
  </Button.Transitions>
</Button>

Rules of thumb:

<Style Selector="Button:pointerover">
  <Setter Property="Opacity" Value="1"/>
  <Setter Property="RenderTransform">
    <Setter.Value>
      <ScaleTransform ScaleX="1.02" ScaleY="1.02"/>
    </Setter.Value>
  </Setter>
</Style>

When the property switches, the matching Transition<T> eases between the two values. Avalonia ships transitions for numeric types, brushes, thickness, transforms, box shadows, and more (Animation/Transitions/*.cs).

Animator-driven transitions

AnimatorDrivenTransition lets you reuse keyframe logic as an implicit transition. Add an Animation to Transition by setting Property and plugging a custom Animator<T> if you need non-linear interpolation or multi-stop blends.

4. Page transitions and content choreography

Navigation surfaces (TransitioningContentControl, Frame, NavigationView) rely on IPageTransition (PageSlide.cs, CrossFade.cs).

<TransitioningContentControl Content="{Binding CurrentPage}">
  <TransitioningContentControl.PageTransition>
    <CompositePageTransition>
      <CompositePageTransition.PageTransitions>
        <PageSlide Duration="0:0:0.25" Orientation="Horizontal" Offset="32"/>
        <CrossFade Duration="0:0:0.20"/>
      </CompositePageTransition.PageTransitions>
    </CompositePageTransition>
  </TransitioningContentControl.PageTransition>
</TransitioningContentControl>

For navigation stacks, pair page transitions with parameterized view-model lifetimes so you can cancel transitions on route changes (TransitioningContentControl.cs).

5. Reactive animation flows

Because each animation pipes through IObservable<bool> internally, you can stitch motion into reactive pipelines:

Example: flash a text box when validation fails, but only once every second.

var throttle = validationFailures
    .Select(_ => true)
    .Throttle(TimeSpan.FromSeconds(1))
    .StartWith(false);
animation.Apply(textBox, null, throttle, null);

6. Composition vs classic rendering

Avalonia's compositor (Compositor.cs) mirrors the Windows Composition model: a scene graph of CompositionVisual objects runs on a dedicated thread and talks directly to GPU backends. Advantages:

Getting the compositor:

var elementVisual = ElementComposition.GetElementVisual(myControl);
var compositor = elementVisual?.Compositor;

You can inject custom visuals under an existing control:

var compositor = ElementComposition.GetElementVisual(host)!.Compositor;
var root = ElementComposition.GetElementVisual(host) as CompositionContainerVisual;

var sprite = compositor.CreateSolidColorVisual();
sprite.Color = Colors.DeepSkyBlue;
sprite.Size = new Vector2((float)host.Bounds.Width, 4);
sprite.Offset = new Vector3(0, (float)host.Bounds.Height - 4, 0);
root!.Children.Add(sprite);

When mixing visuals, ensure they come from the same Compositor instance (ElementCompositionPreview.cs).

Composition target and hit testing

CompositionTarget (CompositionTarget.cs) owns the visual tree that the compositor renders. It handles hit testing, coordinate transforms, and redraw scheduling. Most apps use the compositor implicitly via the built-in renderer, but custom hosts (e.g., embedding Avalonia) can create their own target (Compositor.CreateCompositionTarget).

Composition brushes, effects, and materials

The compositor supports more than simple solids:

Use Compositor.TryCreateBlurEffect() (platform-provided helpers) to probe support before enabling expensive effects. Not every backend exposes every effect type; guard features behind capability checks.

Backend considerations

Composition runs on different engines per platform:

When features are missing, prefer classic transitions so the experience remains functional.

7. Composition animations and implicit animations

Composition animations live in Avalonia.Rendering.Composition.Animations:

Example: create a parallax highlight that lags slightly behind its host.

var compositor = ElementComposition.GetElementVisual(header)!.Compositor;
var hostVisual = ElementComposition.GetElementVisual(header)!;

var glow = compositor.CreateSolidColorVisual();
glow.Color = Colors.Gold;
glow.Size = new Vector2((float)header.Bounds.Width, 4);
ElementComposition.SetElementChildVisual(header, glow);

var parallax = compositor.CreateExpressionAnimation("Vector3(host.Offset.X * 0.05, host.Offset.Y * 0.05, 0)");
parallax.SetReferenceParameter("host", hostVisual);
parallax.Target = nameof(CompositionVisual.Offset);
glow.StartAnimation(nameof(CompositionVisual.Offset), parallax);

For property-driven motion, use implicit animations: create an ImplicitAnimationCollection, add an animation keyed by the composition property name (for example nameof(CompositionVisual.Opacity)), then assign the collection to visual.ImplicitAnimations. Each time that property changes, the compositor automatically plays the animation using this.FinalValue inside the expression to reference the target value (ImplicitAnimationCollection.cs).

StartAnimation pushes the animation to the render thread. Use CompositionAnimationGroup to start multiple animations atomically, and Compositor.RequestCommitAsync() to flush batched changes before measuring results.

8. Performance and diagnostics

9. Practice lab: motion system

  1. Explicit keyframes – Build a reusable animation resource that pulses a NotificationBanner, then start it from a view model with RunAsync. Add cancellation so repeated notifications restart smoothly.
  2. Implicit hover transitions – Define a Transitions block for cards in a dashboard: fade elevation shadows, scale the card slightly, and update TranslateTransform.Y. Drive the transitions purely from pseudo-classes.
  3. Navigation choreography – Wrap your page host in a TransitioningContentControl. Combine PageSlide with CrossFade, listen for TransitionCompleted, and cancel transitions when the navigation stack pops quickly.
  4. Composition parallax – Build a composition child visual that lags behind its host using an expression animation, then snap it back with an implicit animation when pointer capture is lost.
  5. Diagnostics – Toggle renderer diagnostics overlays, capture a short trace, and confirm that the animations remain smooth when background tasks run.

Document timing curves, easing choices, and any performance issues so the team can iterate on the experience.

10. Troubleshooting & best practices

Look under the hood (source bookmarks)

Check yourself

What's next