Skip to main content
Avalonia’s composition system provides a low-level API for creating high-performance visual effects and animations. It separates the UI thread from the render thread, enabling smooth animations even when the UI thread is busy.

Composition Architecture

The composition system consists of two main components:
  1. UI Thread (Compositor) - Manages composition objects and batches changes
  2. Render Thread (ServerCompositor) - Processes batches and renders visuals
From ~/workspace/source/src/Avalonia.Base/Rendering/Composition/Compositor.cs:
public partial class Compositor
{
    internal IRenderLoop Loop { get; }
    private readonly ServerCompositor _server;
    private CompositionBatch? _nextCommit;
    private readonly BatchStreamObjectPool<object?> _batchObjectPool;
    private readonly BatchStreamMemoryPool _batchMemoryPool;
    
    public Task RequestCommitAsync() => 
        RequestCompositionBatchCommitAsync().Processed;
}

Getting Started with Composition

Accessing the Compositor

using Avalonia.Rendering.Composition;
using Avalonia.Media;

public class MyControl : Control
{
    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
    {
        base.OnAttachedToVisualTree(e);
        
        var compositor = ElementComposition.GetElementVisual(this)?.Compositor;
        if (compositor != null)
        {
            SetupComposition(compositor);
        }
    }
    
    private void SetupComposition(Compositor compositor)
    {
        var visual = compositor.CreateContainerVisual();
        visual.Size = new Vector(200, 200);
        visual.Offset = new Vector3D(50, 50, 0);
        
        ElementComposition.SetElementChildVisual(this, visual);
    }
}

Composition Objects

Visual Types

From ~/workspace/source/src/Avalonia.Base/Rendering/Composition/: ContainerVisual
public class ContainerVisual : Visual
{
    public VisualCollection Children { get; }
}
A visual that can contain child visuals. CompositionDrawListVisual
public class CompositionDrawListVisual : Visual
{
    // Renders using a pre-recorded draw list
}
Optimized for static or infrequently changing content. CompositionCustomVisual
public class CompositionCustomVisual : Visual
{
    // Custom rendering via handler
}
Allows custom rendering logic.

Visual Properties

var visual = compositor.CreateContainerVisual();

// Transform properties
visual.Offset = new Vector3D(x, y, z);
visual.Size = new Vector(width, height);
visual.AnchorPoint = new Vector3D(0.5, 0.5, 0); // Center
visual.RotationAngle = Math.PI / 4; // 45 degrees
visual.Scale = new Vector3D(2, 2, 1); // 2x scale
visual.TransformMatrix = Matrix.CreateRotation(angle);

// Appearance properties
visual.Opacity = 0.5;
visual.IsVisible = true;
visual.ClipToBounds = true;

// Advanced properties
visual.OpacityMask = compositor.CreateGradientBrush(...);
visual.Effect = compositor.CreateBlurEffect(radius: 10);

Composition Animations

Composition animations run on the render thread, independent of the UI thread.

Creating Animations

using Avalonia.Rendering.Composition.Animations;

private void AnimateVisual(Compositor compositor, Visual visual)
{
    // Create animation
    var animation = compositor.CreateVector3DKeyFrameAnimation();
    
    // Define keyframes
    animation.InsertKeyFrame(0.0f, new Vector3D(0, 0, 0));
    animation.InsertKeyFrame(0.5f, new Vector3D(100, 0, 0));
    animation.InsertKeyFrame(1.0f, new Vector3D(100, 100, 0));
    
    // Configure animation
    animation.Duration = TimeSpan.FromSeconds(2);
    animation.IterationBehavior = AnimationIterationBehavior.Forever;
    animation.Direction = AnimationDirection.Alternate;
    
    // Start animation
    visual.StartAnimation("Offset", animation);
}

Animation Types

Keyframe Animations
// Scalar (double) animation
var scalarAnimation = compositor.CreateScalarKeyFrameAnimation();
scalarAnimation.InsertKeyFrame(0.0f, 0.0);
scalarAnimation.InsertKeyFrame(1.0f, 360.0);
visual.StartAnimation("RotationAngle", scalarAnimation);

// Vector2D animation
var vector2Animation = compositor.CreateVector2DKeyFrameAnimation();
vector2Animation.InsertKeyFrame(0.0f, new Vector(100, 100));
vector2Animation.InsertKeyFrame(1.0f, new Vector(200, 200));
visual.StartAnimation("Size", vector2Animation);

// Color animation
var colorAnimation = compositor.CreateColorKeyFrameAnimation();
colorAnimation.InsertKeyFrame(0.0f, Colors.Red);
colorAnimation.InsertKeyFrame(1.0f, Colors.Blue);
solidColorBrush.StartAnimation("Color", colorAnimation);

Easing Functions

animation.InsertKeyFrame(1.0f, targetValue, 
    compositor.CreateCubicBezierEasingFunction(
        new Vector(0.42, 0.0),  // Control point 1
        new Vector(0.58, 1.0)   // Control point 2
    ));

// Or use built-in easings
animation.InsertKeyFrame(1.0f, targetValue,
    compositor.CreateLinearEasingFunction());

Expression Animations

Expression animations compute values based on properties of other objects:
var expression = compositor.CreateExpressionAnimation(
    "visual1.Offset.X + 100");
expression.SetReferenceParameter("visual1", otherVisual);

visual.StartAnimation("Offset.X", expression);
Common expressions:
// Follow another visual with offset
"target.Offset + Vector3D(50, 0, 0)"

// Scale based on parent size
"parent.Size.X / 2"

// Animate based on time
"Sin(Time.TotalSeconds * 2 * Pi) * 100"

// Conditional animation
"visual.Opacity > 0.5 ? 1.0 : 0.0"

Composition Brushes

Solid Color Brush

var brush = compositor.CreateColorBrush(Colors.Blue);
visual.OpacityMask = brush;

// Animate brush color
var colorAnim = compositor.CreateColorKeyFrameAnimation();
colorAnim.InsertKeyFrame(1.0f, Colors.Red);
colorAnim.Duration = TimeSpan.FromSeconds(1);
brush.StartAnimation("Color", colorAnim);

Gradient Brushes

var gradientBrush = compositor.CreateLinearGradientBrush();
gradientBrush.StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative);
gradientBrush.EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative);
gradientBrush.GradientStops.Add(
    compositor.CreateColorGradientStop(0.0f, Colors.Blue));
gradientBrush.GradientStops.Add(
    compositor.CreateColorGradientStop(1.0f, Colors.Green));

Custom Composition Visuals

Creating Custom Rendering

public class CustomCompositionHandler : CompositionCustomVisualHandler
{
    public override void OnMessage(object message)
    {
        // Handle messages from UI thread
    }
    
    public override void OnRender(ICompositionDrawingSurface surface)
    {
        using (var context = surface.CreateDrawingContext())
        {
            // Custom rendering code
            context.DrawRectangle(
                Brushes.Red, 
                null, 
                new Rect(0, 0, 100, 100));
        }
    }
    
    public override void OnAnimationFrameUpdate()
    {
        // Called on every frame when animation is running
        Invalidate();
    }
}

// Usage
var customVisual = compositor.CreateCustomVisual(new CustomCompositionHandler());
customVisual.Size = new Vector(200, 200);
ElementComposition.SetElementChildVisual(control, customVisual);

// Send message to handler
customVisual.SendHandlerMessage(new { Type = "Update", Data = 42 });

Composition Effects

Blur Effect

var blurEffect = compositor.CreateBlurEffect();
blurEffect.BlurRadius = 10;
blurEffect.Source = compositor.CreateVisualSurface(sourceVisual);

var effectBrush = compositor.CreateEffectBrush(blurEffect);
visual.OpacityMask = effectBrush;

// Animate blur radius
var blurAnimation = compositor.CreateScalarKeyFrameAnimation();
blurAnimation.InsertKeyFrame(0.0f, 0);
blurAnimation.InsertKeyFrame(1.0f, 20);
blurAnimation.Duration = TimeSpan.FromSeconds(1);
blurEffect.StartAnimation("BlurRadius", blurAnimation);

Batch Commits

Changes to composition objects are batched and committed atomically.

Manual Commit

private async Task UpdateCompositionAsync(Compositor compositor)
{
    // Make multiple changes
    visual1.Offset = new Vector3D(100, 100, 0);
    visual2.Opacity = 0.5;
    visual3.Size = new Vector(200, 200);
    
    // Request commit and wait for processing
    await compositor.RequestCommitAsync();
    
    // Changes are now visible on render thread
}

Automatic Commit

By default, changes are committed automatically at the end of each render loop iteration.

Performance Optimization

Property Sets

Use property sets to avoid creating multiple animations:
var properties = compositor.CreateAnimationPropertySet();
properties.InsertScalar("Progress", 0);

// Animate the property set
var animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(1.0f, 1.0);
animation.Duration = TimeSpan.FromSeconds(2);
properties.StartAnimation("Progress", animation);

// Reference in expressions
var expr1 = compositor.CreateExpressionAnimation(
    "props.Progress * 100");
expr1.SetReferenceParameter("props", properties);
visual1.StartAnimation("Offset.X", expr1);

var expr2 = compositor.CreateExpressionAnimation(
    "props.Progress * 200");
expr2.SetReferenceParameter("props", properties);
visual2.StartAnimation("Offset.X", expr2);

Visual Surfaces

Cache rendered content using visual surfaces:
var surface = compositor.CreateVisualSurface();
surface.SourceVisual = complexVisual;
surface.SourceSize = new Vector(width, height);

// Use surface as texture
var surfaceBrush = compositor.CreateSurfaceBrush(surface);
cachedVisual.OpacityMask = surfaceBrush;
Use visual surfaces to cache expensive rendering operations:
  • Complex vector graphics
  • Text rendering
  • Multiple layers with effects
  • Content that doesn’t change frequently

Composition Options

var options = new CompositionOptions
{
    // Enable/disable specific features
    UseRegionTracking = true,
    
    // Memory management
    MaxResourceCacheSize = 128 * 1024 * 1024, // 128 MB
};

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .With(options)
    .StartWithClassicDesktopLifetime(args);

Advanced Scenarios

Synchronized Animations

public class SynchronizedAnimations
{
    public void CreateSynchronizedFade(
        Compositor compositor,
        Visual visual1,
        Visual visual2)
    {
        var animation = compositor.CreateScalarKeyFrameAnimation();
        animation.InsertKeyFrame(0.0f, 1.0);
        animation.InsertKeyFrame(1.0f, 0.0);
        animation.Duration = TimeSpan.FromSeconds(1);
        
        // Both visuals share the same animation timeline
        visual1.StartAnimation("Opacity", animation);
        visual2.StartAnimation("Opacity", animation);
    }
}

Implicit Animations

Animations that start automatically when a property changes:
var implicitAnimation = compositor.CreateVector3DKeyFrameAnimation();
implicitAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
implicitAnimation.Duration = TimeSpan.FromMilliseconds(300);
implicitAnimation.Target = "Offset";

var implicitAnimations = compositor.CreateImplicitAnimationCollection();
implicitAnimations["Offset"] = implicitAnimation;

visual.ImplicitAnimations = implicitAnimations;

// Now any change to Offset will automatically animate
visual.Offset = new Vector3D(100, 100, 0); // Animates smoothly

Interaction Animations

Create animations that respond to user input:
public class InteractionAnimation
{
    private Visual _visual;
    private Compositor _compositor;
    
    public void SetupDragAnimation()
    {
        // Create expression that follows pointer position
        var expression = _compositor.CreateExpressionAnimation(
            "Vector3D(pointer.Position.X, pointer.Position.Y, 0)");
        
        // Start during pointer press
        _visual.StartAnimation("Offset", expression);
    }
}

Best Practices

Thread Safety: Composition objects are not thread-safe. Always access them from the UI thread, except during custom visual rendering.

When to Use Composition

Use composition for:
  • Smooth animations independent of UI thread
  • Complex visual effects
  • High-performance scrolling scenarios
  • Custom rendering that needs to run at 60+ FPS
  • Interactive animations responding to input
Use standard controls for:
  • Simple UI layouts
  • Standard control animations
  • Data binding scenarios
  • Form-based applications

Memory Management

// Dispose visuals when no longer needed
private void CleanupComposition()
{
    _visual?.Dispose();
    _animation?.Dispose();
    _brush?.Dispose();
}

protected override void OnDetachedFromVisualTree(
    VisualTreeAttachmentEventArgs e)
{
    CleanupComposition();
    base.OnDetachedFromVisualTree(e);
}

Debugging Composition

Enable Composition Debugging

// Set environment variable before app starts
Environment.SetEnvironmentVariable(
    "AVALONIA_COMPOSITION_DEBUG", "1");

// Or enable specific diagnostics
var compositor = ElementComposition.GetElementVisual(this)?.Compositor;
if (compositor?.Server is ICompositionTargetDebugEvents debug)
{
    debug.RenderComplete += (s, e) => 
        Console.WriteLine($"Frame rendered in {e.RenderTime}ms");
}

See Also

Build docs developers (and LLMs) love