Building Cross-Platform Desktop Apps with Avalonia UI

For years, desktop development in .NET meant Windows-only WPF or the increasingly outdated Windows Forms. If you needed cross-platform, you were looking at Electron (hello, 500 MB RAM for a to-do app) or going native per platform. Then Avalonia UI came along and changed the equation entirely.

I’ve been using Avalonia in production for the past year, shipping a developer tools suite that runs on Windows, macOS, and Linux from a single codebase. Here’s what the experience has been like.

What Is Avalonia UI?

Avalonia is an open-source, cross-platform UI framework for .NET. If you know WPF or UWP, you’ll feel right at home — it uses XAML, supports data binding, styles, templates, and has an MVVM-friendly architecture. But unlike WPF, it renders on all major desktop platforms — plus the browser via WebAssembly and mobile via iOS/Android.

Think of it as “WPF, but everywhere.”

Why Not MAUI?

This is the first question I get. .NET MAUI is Microsoft’s official cross-platform framework, so why choose Avalonia?

AspectMAUIAvalonia
Desktop platformsWindows, macOSWindows, macOS, Linux
Linux supportNot supportedFirst-class
RenderingNative controls per platformCustom Skia-based rendering
Look & feelNative per platformConsistent (pixel-perfect)
XAML styleUWP-inspiredWPF-inspired
Maturity on desktopMobile-firstDesktop-first

For my use case — a developer tool that needed to look and behave identically across platforms, including Linux — Avalonia was the clear winner. MAUI is a great choice if you need native platform look-and-feel and don’t need Linux. But for consistent, pixel-perfect desktop apps, Avalonia wins.

Project Setup

Getting started takes about 30 seconds:

dotnet new install Avalonia.Templates
dotnet new avalonia.mvvm -n MyDesktopApp
cd MyDesktopApp
dotnet run

You’ll get a window on screen, regardless of your OS. The template sets up an MVVM structure with ViewModels/ and Views/ directories.

The MVVM Pattern in Avalonia

If you’re coming from WPF, the MVVM pattern is almost identical. I use CommunityToolkit.Mvvm for source-generated properties and commands:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyDesktopApp.ViewModels;

public partial class MainWindowViewModel : ObservableObject
{
    [ObservableProperty]
    private string _searchQuery = string.Empty;

    [ObservableProperty]
    private bool _isLoading;

    [ObservableProperty]
    private ObservableCollection<ProjectItem> _projects = [];

    [RelayCommand]
    private async Task SearchAsync()
    {
        IsLoading = true;
        try
        {
            var results = await _projectService
                .SearchAsync(SearchQuery);
            Projects = new ObservableCollection<ProjectItem>(results);
        }
        finally
        {
            IsLoading = false;
        }
    }
}

The [ObservableProperty] attribute generates the property with INotifyPropertyChanged support. [RelayCommand] generates an ICommand that can be bound directly in XAML. Zero boilerplate.

Styling: Themes and Custom Controls

Avalonia ships with two built-in themes: Fluent (Windows 11-inspired) and Simple. There’s also a community theme called Material.Avalonia that implements Material Design.

For my app, I went with Fluent as a base and customized it heavily:

<Application.Styles>
    <FluentTheme />
    <StyleInclude Source="/Styles/AppTheme.axaml" />
</Application.Styles>

Custom styling uses a CSS-like syntax that’s more powerful than WPF’s:

<Style Selector="Button.primary">
    <Setter Property="Background" Value="#7ee787" />
    <Setter Property="Foreground" Value="#0d1117" />
    <Setter Property="CornerRadius" Value="8" />
    <Setter Property="Padding" Value="16,10" />
    <Setter Property="FontWeight" Value="Bold" />
</Style>

<Style Selector="Button.primary:pointerover /template/ ContentPresenter">
    <Setter Property="Background" Value="#6bd875" />
</Style>

<Style Selector="Button.primary:pressed /template/ ContentPresenter">
    <Setter Property="Background" Value="#5cc869" />
</Style>

The CSS-style selectors (:pointerover, :pressed) are a massive upgrade over WPF’s verbose trigger system.

Data Templates and Lists

Here’s a real example from my app — a project list with a custom item template:

<ListBox Items="{Binding Projects}" 
         SelectedItem="{Binding SelectedProject}">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="vm:ProjectItem">
            <Border Padding="12" Margin="4"
                    CornerRadius="8"
                    Background="{DynamicResource CardBackground}">
                <StackPanel Spacing="6">
                    <DockPanel>
                        <TextBlock Text="{Binding Name}" 
                                   FontWeight="Bold"
                                   FontSize="14"
                                   DockPanel.Dock="Left" />
                        <TextBlock Text="{Binding Language}"
                                   Foreground="{DynamicResource AccentText}"
                                   HorizontalAlignment="Right"
                                   FontFamily="{StaticResource MonoFont}" />
                    </DockPanel>
                    <TextBlock Text="{Binding Description}"
                               Foreground="{DynamicResource SecondaryText}"
                               TextWrapping="Wrap"
                               FontSize="12" />
                    <StackPanel Orientation="Horizontal" Spacing="8">
                        <PathIcon Data="{StaticResource StarIcon}" 
                                  Width="12" Height="12" />
                        <TextBlock Text="{Binding Stars}" FontSize="11" />
                        <PathIcon Data="{StaticResource ForkIcon}" 
                                  Width="12" Height="12" />
                        <TextBlock Text="{Binding Forks}" FontSize="11" />
                    </StackPanel>
                </StackPanel>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Platform-Specific Code

Sometimes you need platform-specific behavior. Avalonia handles this cleanly:

public static class PlatformHelper
{
    public static void OpenInBrowser(string url)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            Process.Start(new ProcessStartInfo(url) 
            { 
                UseShellExecute = true 
            });
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            Process.Start("open", url);
        }
        else
        {
            Process.Start("xdg-open", url);
        }
    }
}

For most things — file dialogs, clipboard, notifications — Avalonia provides cross-platform abstractions so you rarely need this.

Hot Reload and Developer Experience

Avalonia supports XAML Hot Reload through the Avalonia Dev Tools preview. Change your XAML, save, and see the result instantly — no recompile needed. It’s not yet as polished as WPF’s hot reload, but it’s improving rapidly.

The built-in DevTools window (press F12 in debug mode) is excellent — it’s like browser DevTools for your desktop app. You can inspect the visual tree, view computed styles, and debug layout issues.

Packaging and Distribution

Building for all platforms from CI is straightforward:

# GitHub Actions — build for all platforms
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: windows-latest
            rid: win-x64
            ext: .exe
          - os: ubuntu-latest
            rid: linux-x64
            ext: ""
          - os: macos-latest
            rid: osx-arm64
            ext: ""
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "9.0.x"
      - run: >
          dotnet publish src/MyDesktopApp
          -c Release
          -r ${{ matrix.rid }}
          --self-contained true
          -p:PublishSingleFile=true
          -p:IncludeNativeLibrariesForSelfExtract=true

The result is a single executable per platform — no installer needed. On macOS, you can wrap it in a .app bundle. On Linux, you can create an AppImage or distribute the binary directly.

Performance

Avalonia renders via Skia (the same engine Chrome and Flutter use), which means:

  • GPU-accelerated rendering on all platforms
  • Consistent 60fps for standard UI operations
  • Startup time around 200–400ms for a medium-complexity app

For comparison, an equivalent Electron app starts in 2–3 seconds and uses 3–4x the memory. A native WPF app starts slightly faster on Windows, but you’re locked to one platform.

One real benchmark from my app:

MetricAvaloniaElectron equivalent
Cold start350ms2.8s
Memory (idle)85 MB340 MB
Bundle size45 MB180 MB

Limitations to Know

Avalonia isn’t perfect. Here are the rough edges I’ve encountered:

  1. Ecosystem size — The NuGet ecosystem for Avalonia controls is smaller than WPF’s. You’ll occasionally need to build custom controls that would be a NuGet install in WPF.

  2. Documentation gaps — The docs are comprehensive for basics but thin for advanced scenarios. The GitHub discussions and Telegram community fill the gap.

  3. macOS code signing — Distributing on macOS requires Apple Developer enrollment and notarization. This isn’t Avalonia-specific, but it adds friction.

  4. No visual designer — Unlike WPF with Visual Studio’s designer, Avalonia relies on previewer plugins (VS Code, Rider, VS). They work well but aren’t as polished.

When to Choose Avalonia

Use Avalonia when:

  • You need Linux support (MAUI doesn’t have it)
  • You want pixel-perfect consistent UI across platforms
  • Your team already knows WPF/XAML
  • You want a lightweight alternative to Electron
  • You’re building developer tools, internal utilities, or data-intensive desktop apps

Stick with WPF when:

  • You’re Windows-only and need the deepest platform integration
  • You depend on third-party WPF control libraries

Consider MAUI when:

  • You primarily target mobile (iOS/Android) with some desktop
  • Native platform appearance is more important than consistency

Wrapping Up

Avalonia has matured into a production-ready framework for cross-platform desktop development in .NET. It’s not trying to replace web apps or mobile — it’s filling the gap that’s existed for years: high-quality desktop applications that run everywhere, built with familiar .NET tools.

For .NET developers who’ve been stuck on Windows-only WPF or forced into Electron, it’s worth a serious look. The developer experience is already excellent, and the community is growing fast.


Building a cross-platform .NET app and want to compare notes? Connect with me on LinkedIn.