Goal
Why this matters
Prerequisites
Install wasm-tools workload:
sudo dotnet workload install wasm-tools
A multi-target solution has:
MyApp): Avalonia code.MyApp.Browser): hosts the app (Program.cs, index.html, static assets).Avalonia template (dotnet new avalonia.app --multiplatform) can create the browser head for you. MyApp.Browser references Avalonia.Browser, which wraps the WebAssembly host (BrowserAppBuilder, BrowserSingleViewLifetime, BrowserNativeControlHost).
When adding the head manually, target net8.0-browserwasm, configure <WasmMainJSPath>wwwroot/main.js</WasmMainJSPath>, and keep trimming hints (e.g., <InvariantGlobalization>true</InvariantGlobalization>). Browser heads use the NativeAOT toolchain; Release builds can set <PublishAot>true</PublishAot> for faster startup and smaller payloads.
StartBrowserAppAsync attaches Avalonia to a DOM element by ID.
using Avalonia;
using Avalonia.Browser;
internal sealed class Program
{
private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
public static Task Main(string[] args)
=> BuildAvaloniaApp()
.StartBrowserAppAsync("out");
}
Ensure host HTML contains <div id="out"></div>.
For advanced embedding, use BrowserAppBuilder directly:
await BrowserAppBuilder.Configure<App>()
.SetupBrowserAppAsync(options =>
{
options.MainAssembly = typeof(App).Assembly;
options.AppBuilder = AppBuilder.Configure<App>().LogToTrace();
options.Selector = "#out";
});
SetupBrowserAppAsync lets you delay instantiation (wait for configuration, auth, etc.) or mount multiple roots in different DOM nodes.
Browser uses ISingleViewApplicationLifetime (same as mobile). Configure in App.OnFrameworkInitializationCompleted:
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
singleView.MainView = new ShellView { DataContext = new ShellViewModel() };
else if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow { DataContext = new ShellViewModel() };
base.OnFrameworkInitializationCompleted();
}
Navigation patterns from Chapter 19 apply (content control with back stack).
Configure BrowserPlatformOptions to choose rendering mode and polyfills.
await BuildAvaloniaApp().StartBrowserAppAsync(
"out",
new BrowserPlatformOptions
{
RenderingMode = new[]
{
BrowserRenderingMode.WebGL2,
BrowserRenderingMode.WebGL1,
BrowserRenderingMode.Software2D
},
RegisterAvaloniaServiceWorker = true,
AvaloniaServiceWorkerScope = "/",
PreferFileDialogPolyfill = false,
PreferManagedThreadDispatcher = true
});
PreferManagedThreadDispatcher: run dispatcher on worker thread when WASM threading enabled (requires server sending COOP/COEP headers).PreferFileDialogPolyfill: toggle between File System Access API and download/upload fallback for unsupported browsers.IStorageProvider uses the File System Access API when available; otherwise a polyfill (service worker + download anchor) handles saves.
Limitations:
Example save using polyfill-friendly code (Chapter 16 shows full pattern). Test with/without service worker to ensure both paths work.
Clipboard operations require user gestures and may only support text formats.
Clipboard.SetTextAsync works after user interaction (button click).Drag/drop from browser to app is supported, but dragging files out of the app is limited by browser APIs.
fetch. All requests obey CORS. Configure server with correct Access-Control-Allow-* headers.ClientWebSocket if server enables them.HttpClient respects browser caching rules. Adjust Cache-Control headers or add cache-busting query parameters during development to avoid stale responses.Call JS via window.JSObject or JSRuntime helpers (Avalonia.Browser exposes interop helpers). Example:
using Avalonia.Browser.Interop;
await JSRuntime.InvokeVoidAsync("console.log", "Hello from Avalonia");
Use interop to integrate with existing web components or to access Web APIs not wrapped by Avalonia.
To host native DOM content inside Avalonia, use BrowserNativeControlHost with a JSObjectControlHandle:
var handle = await JSRuntime.CreateControlHandleAsync("div", new { @class = "web-frame" });
var host = new BrowserNativeControlHost { Handle = handle };
This enables hybrid UI scenarios (rich HTML editors, video elements) while keeping sizing/layout under Avalonia control.
Avalonia.Browser.Blazor lets you embed Avalonia controls in a Blazor app. Example sample: ControlCatalog.Browser.Blazor. Use when you need Blazor's routing/layout but Avalonia UI inside components.
AppBundle and serve from any static host (GitHub Pages, S3 + CloudFront, Azure Static Web Apps). Ensure service worker scope matches site root.MapFallbackToFile("index.html") or UseBlazorFrameworkFiles() to serve the bundle from a Minimal API or MVC backend.Cross-Origin-Embedder-Policy/Cross-Origin-Opener-Policy headers when enabling multithreaded WASM.During development, dotnet run on the browser head launches a Kestrel server with live reload and proxies console logs back to the terminal.
dotnet publish -c Debug to get wasm debugging symbols for supported browsers.AppBuilder.LogToTrace() outputs to console.--logger:WebAssembly to dotnet run for runtime messages (assembly loading, exception details).wasm-tools wasm-strip or wasm-tools wasm-opt (installed via dotnet wasm build-tools --install) to analyze and reduce bundle sizes.AppBundle, track .wasm, .dat, and compressed assets.BrowserSystemNavigationManager with your navigation service so browser back/forward controls work as expected.Publish the browser head:
cd MyApp.Browser
# Debug
dotnet run
# Release bundle
dotnet publish -c Release
Output under bin/Release/net8.0/browser-wasm/AppBundle. Serve via static web server (ASP.NET, Node, Nginx, GitHub Pages). Ensure service worker scope matches hosting path.
Remember to enable compression (Brotli) for faster load times.
| Feature | Browser behavior |
|---|---|
| Windows/Dialogs | Single view only; no OS windows, tray icons, native menus |
| File system | User-selection only via pickers; no arbitrary file access |
| Threading | Multi-threaded WASM requires server headers (COOP/COEP) and browser support |
| Clipboard | Requires user gesture; limited formats |
| Notifications | Use Web Notifications API via JS interop |
| Storage | LocalStorage/IndexedDB via JS interop for persistence |
Design for progressive enhancement: provide alternative flows if feature unsupported.
IStorageProvider and test save polyfill with service worker enabled/disabled.BrowserPlatformOptions.RenderingMode and ActualTransparencyLevel (should be None).wasm-tools wasm-strip (or wasm-opt) to inspect bundle size before/after trimming and record the change.BrowserAppBuilder.csJSObjectControlHandle.csBrowserSingleViewLifetime.csBrowserNativeControlHost.csBrowserStorageProvider.csBrowserSystemNavigationManager.csBrowserInputPane.cs, BrowserInsetsManager.csAvalonia.Browser.BlazorPreferManagedThreadDispatcher?What's next