Avalonia for WebAssembly

The real .NET runtime, your existing C# and XAML, drawn onto a canvas and served as static files. The same UI code that runs your desktop build.

Platform support

Your desktop application, served as static files.

Avalonia compiles the C# and XAML you already have to WebAssembly and draws it onto a canvas. There’s no HTML to author, no CSS cascade to fight and no server-side component to deploy.

Hosting

Static files

No server-side component is required. Any static host will do.

Rendering

Skia through CanvasKit

Drawn to a WebGL canvas, so there’s no DOM to fight.

Browsers

Anything with full WebAssembly

The docs defer to caniuse.com/wasm rather than pinning versions.

Runtime

.NET 8.0 or later

The real .NET runtime, the real base class library, in the tab.

Running in this tab

This is Avalonia, compiled to WebAssembly

What opens is the Avalonia Charts control, running on the real .NET runtime in this tab. No plugin, no server rendering the frames, and no second UI stack.

The live demo runs on a desktop browser, where spending the download makes sense. Everything describing it below is the same on a phone.

Not a second product

The browser is a publish target, not a second front end.

WebAssembly is another head on the project you already maintain. The view models, the styles, the custom controls and the validation rules are the ones your desktop build uses, because they’re literally the same assemblies.

That removes the usual cost of a web version: a second UI stack, a second place to fix a bug and a second design system to keep in step. The output is a folder of static files, so it deploys to object storage, a CDN or whatever already serves your marketing site.

dotnet publish -c Release
# bin/Release/net8.0-browserwasm/publish/wwwroot

What you get in the tab

The real runtime, not a subset of it.

Avalonia on WebAssembly runs on the .NET runtime rather than on a translation layer, so the language and the library surface are the ones you already know.

The base class library

System.Text.Json, HttpClient, LINQ, generics, async and the rest of the BCL behave the way they do everywhere else, because it’s the same runtime.

NuGet packages

If a package compiles against .NET and doesn’t need a platform capability the browser withholds, it works, using the same package reference as the desktop build.

JavaScript interop where you want it

Call browser APIs or an existing JavaScript library through .NET’s JS interop when the web platform genuinely has something to offer, without restructuring the interface to get there.

Identical rendering

Skia draws through CanvasKit onto a WebGL canvas, so the browser build looks like the desktop build, not an HTML approximation of it.

Every component page runs its control live in the browser, which is itself Avalonia compiled to WebAssembly.

Try more demos

Where it meets the web

Avalonia owns the canvas. The browser is still right there.

Drawing the interface ourselves is what makes the browser build match the desktop one. It doesn’t put the web platform out of reach when you actually want it.

[JSImport("navigator.clipboard.writeText", "host")]
internal static partial Task CopyAsync(string text);

[JSExport]
internal static void OnDroppedFile(string name) =>
    Messenger.Send(new FileDropped(name));

Two attributes, in both directions

JSImport pulls a JavaScript function into C# as an ordinary typed method, and JSExport hands a C# method back to the page. Callbacks marshal as callable functions and object references pass as proxies, so an existing JavaScript library stays usable without rebuilding your UI around it.

await JSHost.ImportAsync("host", "/host.js");

Load your own modules

JSHost.ImportAsync brings a JavaScript module in at startup and names it, which is the name your JSImport attributes then resolve against. Prefix with globalThis to reach something already on the page.

<RunAOTCompilation>true</RunAOTCompilation>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

Ahead of time, when you want the frames

RunAOTCompilation compiles IL before it ships, trading a larger download for markedly faster execution. It’s the right trade for an application people keep open and the wrong one for a page they glance at.

Trimmed by default

Release builds trim automatically, so the linker drops the parts of your assemblies and the framework that your application never calls. That’s size you don’t have to negotiate for.

The compression does real work

The docs put Brotli at a 60 to 70 percent reduction on the .wasm files specifically, and most static hosts apply it without being asked. Serve it with long-lived cache headers and the cost lands once per release.

dotnet workload install wasm-tools

One workload, then build

The wasm-tools workload is the whole toolchain, and the avalonia.xplat template already carries a browser head. A workload install and a template reference are the only two things you add to an existing solution.

Debug it like the desktop build

DevTools inspects the running visual tree, styles and bindings of the browser build with the same tool you use on the desktop one, because underneath it’s the same tree.

Publishing, hosting and the interop attribute reference are all documented.

WebAssembly guide

Getting it deployed

Static files, and one thing to plan for.

The build output is a wwwroot folder. What it costs to deliver that folder is the part worth designing around, so here it is plainly.

Any static host

GitHub Pages, the nginx already running your main site, or any other static host you’ve got. No server-side component is required.

One MIME type to get right

Your server has to send .wasm as application/wasm. Nearly everything modern already does, and it’s the first thing to check when a deployment works everywhere except one environment.

Cache it once per release

The payload is content-addressed and immutable, so long-lived cache headers move the cost from once per visit to once per deploy. That single decision is most of the perceived-speed story.

The honest part: first load

The runtime and your assemblies have to reach the browser before the first frame. Avalonia on the web suits applications people return to, not a landing page that must paint in 200ms.

Good fits

Internal tools, operator consoles, dashboards, demos of desktop software, and anything where a login already precedes the first screen.

Preload what you can

Fetch the runtime while the visitor is still reading, and boot it when they look like they’re about to interact. The demo higher up this page does exactly that, which is why it opens as fast as it does.

Add the browser head to an existing Avalonia solution and publish it to a static host in an afternoon.

Get started

Every target

The browser is the sixth target.

Nothing about the browser build is a separate codebase. It’s the desktop and mobile project with one more head added to the solution.

Publish the same demo you just tried.

Add a browser head to an existing Avalonia project and publish the wwwroot folder to any static host. The framework is MIT licensed, and there’s no server to provision before people can use it.