Skip to main content

Blazor WebAssembly Tutorial 2026: Build Web Apps in C#

Learn Blazor WebAssembly in 2026 with this step-by-step C# tutorial. Build interactive web apps without JavaScript, with code examples and best practices.

If you're a C# developer who has ever groaned at the thought of context-switching into JavaScript, Blazor WebAssembly is the technology you've been waiting for. In 2026, Blazor WebAssembly (often shortened to "Blazor WASM") is a mature, production-ready framework that lets you build fully interactive single-page applications that run entirely in the browser—using nothing but C#, Razor, and .NET. In this Blazor WebAssembly tutorial, you'll learn how it works under the hood, build a working app from scratch with .NET 10, and pick up the best practices and pitfalls that separate a smooth project from a frustrating one.

What Is Blazor WebAssembly and Why Does It Matter in 2026?

Blazor WebAssembly is a client-side hosting model for ASP.NET Core Blazor. Instead of running your C# on a server and pushing UI diffs over a SignalR connection (that's Blazor Server), Blazor WASM ships a .NET runtime compiled to WebAssembly down to the browser. Your compiled assemblies execute inside the browser's sandbox, right next to the JavaScript engine, with no plugin required.

Why does this matter? Because the browser is the only universal application platform, and until WebAssembly arrived, JavaScript was the only language allowed to run there. Blazor WebAssembly breaks that monopoly. The practical benefits are significant:

  • One language, one stack. Share models, validation logic, and business rules between your ASP.NET Core API and your front end. No duplicating DTOs in TypeScript.
  • Offline-capable apps. Since everything runs client-side, Blazor WASM works beautifully as a Progressive Web App (PWA) that keeps working without a network connection.
  • Cheap static hosting. The published output is just static files. Host it on Azure Static Web Apps, GitHub Pages, Cloudflare Pages, or an S3 bucket for pennies.
  • Strong typing and tooling. Compile-time errors, IntelliSense, refactoring, and the entire NuGet ecosystem.

With .NET 10, the story has improved further: smaller download sizes thanks to improved trimming, faster startup via ahead-of-time (AOT) compilation and WebAssembly SIMD, and the unified Blazor Web App template that lets you mix server and WebAssembly rendering per component.

Blazor WebAssembly vs Blazor Server vs React: Which Should You Choose?

Developers searching "blazor vs react" or "blazor server vs webassembly" usually want a straight answer. Here's the honest comparison:

  • Blazor WebAssembly — Best for apps that need offline support, low server cost, or heavy client-side interactivity. Trade-off: larger initial download (typically 1–3 MB compressed after trimming) and no direct database access from the client.
  • Blazor Server — Tiny initial download and full server-side access, but every UI interaction requires a round trip over SignalR. Latency-sensitive and doesn't work offline.
  • React / Angular / Vue — The largest ecosystems and talent pools. Choose these if your team is JavaScript-first or you need a massive library of third-party UI components. Choose Blazor if your team is .NET-first and you value shared code over ecosystem breadth.

The good news: in 2026 you don't have to decide upfront. The Blazor Web App template supports Auto render mode, which renders a component with Blazor Server on first load for instant interactivity, downloads the WebAssembly runtime in the background, and switches to client-side execution on subsequent visits.

Prerequisites

  • .NET 10 SDK (download from dotnet.microsoft.com)
  • Visual Studio 2026, VS Code with the C# Dev Kit, or JetBrains Rider
  • Basic familiarity with C# and HTML

Step 1: Create Your First Blazor WebAssembly App

Open a terminal and run the following commands. The standalone WebAssembly template is the simplest starting point and produces a pure client-side app:

dotnet new blazorwasm -o TaskTracker
cd TaskTracker
dotnet run

Navigate to the URL shown in the console (usually https://localhost:5001). You'll see the default app with a counter and a weather page—all running as C# inside your browser. Open your browser's DevTools Network tab and you'll notice dotnet.wasm and a collection of .wasm assembly files being loaded. That's the .NET runtime.

Let's look at Program.cs, which is the entry point:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using TaskTracker;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// Root components are attached to DOM elements in wwwroot/index.html
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// Register services with dependency injection, exactly as in ASP.NET Core
builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

await builder.Build().RunAsync();

If you've written any ASP.NET Core, this should feel instantly familiar. The same dependency injection container, the same configuration patterns—just running in the browser.

Step 2: Build an Interactive Component

Blazor UIs are built from components, which are .razor files combining HTML markup with C# logic. Let's build a task list component. Create Pages/Tasks.razor:

@page "/tasks"
@using TaskTracker.Models

<PageTitle>My Tasks</PageTitle>

<h1>Task Tracker</h1>

<div class="input-group mb-3">
    <input class="form-control"
           placeholder="What needs doing?"
           @bind="newTaskTitle"
           @bind:event="oninput"
           @onkeydown="HandleKeyDown" />
    <button class="btn btn-primary"
            @onclick="AddTask"
            disabled="@string.IsNullOrWhiteSpace(newTaskTitle)">
        Add
    </button>
</div>

@if (tasks.Count == 0)
{
    <p class="text-muted">No tasks yet. Add one above!</p>
}
else
{
    <ul class="list-group">
        @foreach (var task in tasks)
        {
            <li class="list-group-item d-flex justify-content-between align-items-center">
                <div>
                    <input type="checkbox"
                           class="form-check-input me-2"
                           checked="@task.IsDone"
                           @onchange="() => ToggleTask(task)" />
                    <span style="@(task.IsDone ? "text-decoration: line-through" : "")">
                        @task.Title
                    </span>
                </div>
                <button class="btn btn-sm btn-outline-danger"
                        @onclick="() => RemoveTask(task)">
                    Delete
                </button>
            </li>
        }
    </ul>
    <p class="mt-3">@tasks.Count(t => !t.IsDone) remaining</p>
}

@code {
    private readonly List<TaskItem> tasks = new();
    private string newTaskTitle = string.Empty;

    private void AddTask()
    {
        if (string.IsNullOrWhiteSpace(newTaskTitle)) return;

        tasks.Add(new TaskItem { Id = Guid.NewGuid(), Title = newTaskTitle.Trim() });
        newTaskTitle = string.Empty;
    }

    private void ToggleTask(TaskItem task) => task.IsDone = !task.IsDone;

    private void RemoveTask(TaskItem task) => tasks.Remove(task);

    private void HandleKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Enter") AddTask();
    }
}

And the model in Models/TaskItem.cs:

namespace TaskTracker.Models;

public class TaskItem
{
    public Guid Id { get; set; }
    public required string Title { get; set; }
    public bool IsDone { get; set; }
}

Notice what's happening here. @bind creates two-way data binding between the input and the newTaskTitle field. @bind:event="oninput" makes it update on every keystroke rather than on blur, which is why the Add button enables in real time. @onclick and @onchange wire DOM events straight to C# methods. There is no virtual DOM library to learn, no state management boilerplate—after an event handler runs, Blazor automatically re-renders the component and diffs the output against the real DOM.

Add a link to Layout/NavMenu.razor and run the app. You now have a fully interactive task list written in 100% C#.

Step 3: Call a Web API with HttpClient

Real applications talk to servers. Because Blazor WASM runs in the browser, it uses the browser's fetch API under the hood, exposed through the standard HttpClient you already know. Here's a service that loads tasks from an API:

using System.Net.Http.Json;
using TaskTracker.Models;

namespace TaskTracker.Services;

public class TaskApiClient(HttpClient http)
{
    public async Task<List<TaskItem>> GetTasksAsync(CancellationToken ct = default)
        => await http.GetFromJsonAsync<List<TaskItem>>("api/tasks", ct) ?? [];

    public async Task<TaskItem?> CreateTaskAsync(TaskItem task, CancellationToken ct = default)
    {
        var response = await http.PostAsJsonAsync("api/tasks", task, ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<TaskItem>(cancellationToken: ct);
    }
}

Register it in Program.cs with builder.Services.AddScoped<TaskApiClient>();, then inject and use it in the component:

@inject TaskApiClient Api

@code {
    private List<TaskItem> tasks = [];
    private bool isLoading = true;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            tasks = await Api.GetTasksAsync();
        }
        catch (HttpRequestException ex)
        {
            Console.Error.WriteLine($"Failed to load tasks: {ex.Message}");
        }
        finally
        {
            isLoading = false;
        }
    }
}

Why OnInitializedAsync? Blazor components have a lifecycle. Loading data in the async initialization method means the component renders once immediately (showing a loading state) and again when the data arrives. Never do async work in the constructor—you'd block rendering and lose access to injected services.

One crucial detail: because your app runs in the browser, your API must send proper CORS headers if it lives on a different origin. This is the number-one source of "it works in Postman but not in Blazor" questions.

Step 4: Interop with JavaScript When You Need It

"Build web apps without JavaScript" doesn't mean JavaScript is forbidden—it means you only reach for it when the browser API you need has no .NET wrapper. Blazor's IJSRuntime makes this painless. Suppose you want to persist tasks to localStorage:

@inject IJSRuntime JS

@code {
    private async Task SaveToLocalStorageAsync()
    {
        var json = System.Text.Json.JsonSerializer.Serialize(tasks);
        await JS.InvokeVoidAsync("localStorage.setItem", "tasks", json);
    }

    private async Task LoadFromLocalStorageAsync()
    {
        var json = await JS.InvokeAsync<string?>("localStorage.getItem", "tasks");
        if (!string.IsNullOrEmpty(json))
        {
            tasks = System.Text.Json.JsonSerializer.Deserialize<List<TaskItem>>(json) ?? [];
        }
    }
}

Notice that you didn't write a single line of JavaScript—you're calling built-in browser functions directly by name. For more complex scenarios, you can import ES modules with JS.InvokeAsync<IJSObjectReference>("import", "./scripts/charts.js"). In .NET 10, [JSImport] and [JSExport] attributes also allow source-generated, high-performance interop with zero reflection.

Blazor WebAssembly Best Practices for Production

1. Enable AOT compilation and trimming for real apps

By default, Blazor WASM runs your IL through an interpreter, which is fine for development but slower than native JavaScript for CPU-heavy work. Ahead-of-time compilation converts your assemblies to native WebAssembly, giving 2–5x speedups on compute-bound code:

<!-- In your .csproj -->
<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
  <PublishTrimmed>true</PublishTrimmed>
  <WasmEnableSIMD>true</WasmEnableSIMD>
</PropertyGroup>

The trade-off is a larger download and much longer publish times, so only enable it in release builds and measure whether your app actually benefits.

2. Use lazy loading for large assemblies

Not every user needs your PDF-export or charting library on the first page load. Mark assemblies as BlazorWebAssemblyLazyLoad in the project file and load them on demand with LazyAssemblyLoader when the user navigates to the route that needs them.

3. Use @key in loops

When rendering lists that change, add @key="task.Id" to the repeated element. Without it, Blazor's diffing algorithm may reuse the wrong DOM elements when items are inserted or removed, causing subtle bugs with checkbox state and input focus.

4. Never put secrets in a Blazor WASM app

Everything in a client-side app is downloadable by the user. API keys, connection strings, and business logic you want to protect must stay on the server. Authenticate with OpenID Connect (Microsoft Entra, Auth0, Keycloak) using the Microsoft.AspNetCore.Components.WebAssembly.Authentication package, and authorize on the API.

5. Prefer EventCallback for child-to-parent communication

Instead of passing plain Action delegates, use EventCallback<T> for component parameters. It automatically triggers a re-render of the parent and correctly handles async handlers.

Common Pitfalls and How to Avoid Them

  • Calling StateHasChanged() everywhere. Blazor already re-renders after event handlers and lifecycle methods. You only need it when state changes outside of Blazor's knowledge—for example from a timer or a JS callback.
  • Forgetting to dispose. Components that subscribe to events or timers must implement IDisposable or IAsyncDisposable and unsubscribe, or you'll leak memory in a long-running SPA.
  • Blocking with .Result or .Wait(). The browser is single-threaded. Synchronously blocking on a task will deadlock your app. Always await.
  • Ignoring the initial download size. Check your published wwwroot/_framework folder. If it's over 5 MB compressed, audit your dependencies—a single heavy NuGet package can double your load time.
  • Debugging surprises. Browser debugging works in Visual Studio and VS Code, but hot reload and breakpoints can behave differently than in server apps. If breakpoints don't hit, make sure you're launching with the debugger attached rather than plain dotnet run.

Conclusion: Is Blazor WebAssembly Worth Learning in 2026?

Absolutely. Blazor WebAssembly has grown from an experimental curiosity into a first-class way to build interactive web apps in C#. With .NET 10's performance improvements, Auto render mode, and a thriving component ecosystem (MudBlazor, Radzen, Telerik, Syncfusion), it's a serious alternative to JavaScript frameworks for any .NET team.

Key takeaways from this Blazor WebAssembly tutorial:

  • Blazor WASM runs the .NET runtime inside the browser via WebAssembly—your C# executes client-side with no server round trips for UI updates.
  • Components combine Razor markup with C# logic; @bind, @onclick, and lifecycle methods like OnInitializedAsync handle most of what you need.
  • Use the familiar HttpClient to call APIs and IJSRuntime for the rare cases where you need a browser API without a .NET wrapper.
  • For production, enable AOT and trimming, lazy-load heavy assemblies, use @key in loops, and keep secrets on the server.
  • Avoid blocking calls, dispose your subscriptions, and don't sprinkle StateHasChanged() unnecessarily.

The best way to learn is to build. Take the task tracker from this article, hook it up to a minimal ASP.NET Core API, add authentication, and deploy it to Azure Static Web Apps. In an afternoon you'll have a real, production-grade web app—without writing a single line of JavaScript.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...