Blazor Tutorial for Beginners: Build Your First App Step by Step (.NET 10)

Gulab Chand Tejwani
PMP · PL-300 · Microsoft Certified Trainer
· 12 min read

To build your first Blazor app, install the .NET 10 SDK, run dotnet new blazor -o TaskBoard, then dotnet watch inside the folder. That gives you a working Blazor Web App with interactive server rendering. From there you add a Razor component (a .razor file with HTML and C#), give it the @rendermode InteractiveServer directive so buttons respond, and inject a C# service to hold your data. This tutorial builds a small task board app doing exactly that.
I'm Gulab Chand Tejwani, a Microsoft Certified Trainer, and I teach Blazor at TechNet Consultancy in Anand. This is the same first exercise I use in class. It takes about an hour if you type the code yourself, which I strongly recommend over copying.
What do you need before starting?
- The .NET 10 SDK. Download it from dotnet.microsoft.com. Check it with
dotnet --version; you should see a number starting with 10. - An editor. Visual Studio 2026 on Windows, or VS Code with the C# Dev Kit extension on any operating system.
- Basic C#. Classes, properties, lists and methods. If those feel shaky, spend a week on C# first. My .NET developer roadmap explains what to cover.
You don't need JavaScript. That's one of the main reasons C# developers pick Blazor.
How do you create a Blazor project?
Open a terminal and run:
dotnet new blazor -o TaskBoard
cd TaskBoard
dotnet watch
dotnet new blazor creates a Blazor Web App, the template Microsoft recommends since .NET 8. By default it sets up interactive server rendering, where the UI runs on the server and updates the browser over a SignalR connection. dotnet watch starts the app and reloads it when you save a file. Your browser should open on a page with Home, Counter and Weather links.
What's inside the project?
The files you'll touch most:
Program.cssets up services and the request pipeline.Components/App.razoris the root HTML document.Components/Routes.razorcontains the router that maps URLs to pages.Components/Layout/MainLayout.razoris the shared layout with the navigation menu.Components/Pages/holds your pages. Any component with an@pagedirective becomes a routable page.
Open Program.cs and find these two lines. They're what turn on interactive server components:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
How does a Blazor component work?
Open Components/Pages/Counter.razor. It's short:
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Four ideas in nine lines:
@page "/counter"makes it a page at that URL.@rendermode InteractiveServermakes it interactive. Remove that line and the button stops working, because the page is then rendered statically on the server with no live connection. This is the most common beginner surprise in .NET 8 and later.@currentCountwrites a C# value into the HTML.@onclickwires a button to a C# method. After the method runs, Blazor re-renders the component and sends only the changes to the browser.

The Razor components docs on Microsoft Learn cover this model in detail.
Step 1: Create a model and a service
Make a folder called Models and add TaskItem.cs:
using System.ComponentModel.DataAnnotations;
namespace TaskBoard.Models;
public class TaskItem
{
public int Id { get; set; }
[Required(ErrorMessage = "Give the task a title.")]
[StringLength(80, ErrorMessage = "Keep it under 80 characters.")]
public string Title { get; set; } = "";
public bool IsDone { get; set; }
}
Now a Services folder with TaskService.cs. For this tutorial it keeps tasks in memory. Later you'd swap it for a database with EF Core.
using TaskBoard.Models;
namespace TaskBoard.Services;
public class TaskService
{
private readonly List<TaskItem> _tasks = new();
private int _nextId = 1;
public IReadOnlyList<TaskItem> GetAll() => _tasks;
public void Add(string title)
{
_tasks.Add(new TaskItem { Id = _nextId++, Title = title.Trim() });
}
public void Toggle(int id)
{
var task = _tasks.FirstOrDefault(t => t.Id == id);
if (task is not null) task.IsDone = !task.IsDone;
}
public void Remove(int id) => _tasks.RemoveAll(t => t.Id == id);
}
Register it in Program.cs, before var app = builder.Build();:
builder.Services.AddScoped<TaskBoard.Services.TaskService>();
Why AddScoped? In interactive server apps, a scoped service lives for one user's circuit (their connection), so each browser tab gets its own task list. AddSingleton would share one list across every user, which is rarely what you want for user data.
| Lifetime | Lives for | Use it for |
|---|---|---|
| AddScoped | One user's circuit (browser tab) | Per-user state, data services, most of your app |
| AddSingleton | The whole app, every user | Shared caches, configuration, stateless helpers |
| AddTransient | Each time it is injected | Lightweight, stateless services |
Step 2: Build the task page
Create Components/Pages/Tasks.razor:
@page "/tasks"
@rendermode InteractiveServer
@using TaskBoard.Models
@using TaskBoard.Services
@inject TaskService Tasks
<PageTitle>Task board</PageTitle>
<h1>Task board</h1>
<EditForm Model="newTask" OnValidSubmit="AddTask" FormName="add-task">
<DataAnnotationsValidator />
<div class="d-flex gap-2">
<InputText @bind-Value="newTask.Title" class="form-control"
placeholder="What needs doing?" />
<button type="submit" class="btn btn-primary">Add</button>
</div>
<ValidationMessage For="() => newTask.Title" />
</EditForm>
<p class="mt-3">@Tasks.GetAll().Count(t => !t.IsDone) open of @Tasks.GetAll().Count</p>
<ul class="list-group">
@foreach (var task in Tasks.GetAll())
{
<li class="list-group-item d-flex justify-content-between">
<label>
<input type="checkbox" checked="@task.IsDone"
@onchange="() => Tasks.Toggle(task.Id)" />
<span style="@(task.IsDone ? "text-decoration: line-through" : "")">
@task.Title
</span>
</label>
<button class="btn btn-sm btn-outline-danger"
@onclick="() => Tasks.Remove(task.Id)">Delete</button>
</li>
}
</ul>
@code {
private TaskItem newTask = new();
private void AddTask()
{
Tasks.Add(newTask.Title);
newTask = new TaskItem();
}
}
Go to /tasks. Type a title and press Add. Try submitting an empty title and you'll see the validation message from the [Required] attribute.
What's happening here:
@inject TaskService Tasksasks dependency injection for the service you registered.EditFormwithDataAnnotationsValidatorreads the attributes onTaskItemand blocks invalid submits.OnValidSubmitonly runs when the model is valid.@bind-Valueis two-way binding: typing updatesnewTask.Title, and resettingnewTaskclears the box.- The lambdas in
@onchangeand@onclickpass the task's ID to the service.
Step 3: Add the page to the menu
Open Components/Layout/NavMenu.razor and copy one of the existing NavLink blocks:
<div class="nav-item px-3">
<NavLink class="nav-link" href="tasks">
<span class="bi bi-list-check" aria-hidden="true"></span> Tasks
</NavLink>
</div>
NavLink adds an active CSS class when the URL matches, so the menu highlights the current page without any extra code.
Step 4: Split it into a reusable component
Pages get long quickly. Pull the list item into its own component. Create Components/TaskRow.razor:
@using TaskBoard.Models
<li class="list-group-item d-flex justify-content-between">
<label>
<input type="checkbox" checked="@Item.IsDone" @onchange="() => OnToggle.InvokeAsync(Item.Id)" />
@Item.Title
</label>
<button class="btn btn-sm btn-outline-danger" @onclick="() => OnDelete.InvokeAsync(Item.Id)">Delete</button>
</li>
@code {
[Parameter, EditorRequired] public TaskItem Item { get; set; } = default!;
[Parameter] public EventCallback<int> OnToggle { get; set; }
[Parameter] public EventCallback<int> OnDelete { get; set; }
}
Then the loop in Tasks.razor becomes:
@foreach (var task in Tasks.GetAll())
{
<TaskRow Item="task" OnToggle="Tasks.Toggle" OnDelete="Tasks.Remove" />
}
[Parameter] properties are how a parent passes data down. EventCallback is how a child tells its parent something happened. That parent-child pattern is most of what you need for real Blazor UIs.
Step 5: Load data asynchronously
Real data comes from a database or an API, and that takes time. Blazor components have lifecycle methods for this. The one you'll use most is OnInitializedAsync, which runs when the component first loads.
Add an async method to TaskService that pretends to fetch from a server:
public async Task<IReadOnlyList<TaskItem>> LoadAsync()
{
await Task.Delay(800); // stands in for a database or API call
if (_tasks.Count == 0)
{
Add("Install the .NET 10 SDK");
Add("Finish the Blazor tutorial");
}
return _tasks;
}
Then, in Tasks.razor, load it and show a message while it's working:
@if (items is null)
{
<p><em>Loading tasks…</em></p>
}
else
{
<ul class="list-group">
@foreach (var task in items)
{
<TaskRow Item="task" OnToggle="Tasks.Toggle" OnDelete="Tasks.Remove" />
}
</ul>
}
@code {
private IReadOnlyList<TaskItem>? items;
protected override async Task OnInitializedAsync()
{
items = await Tasks.LoadAsync();
}
}
Blazor renders once with items still null (so the user sees "Loading tasks…"), then renders again when the task finishes. Always handle that first, empty render. Forgetting it is the classic NullReferenceException in beginners' Blazor code.
Step 6: Fix the errors every beginner hits
You'll meet at least two of these in your first week. Here's what they mean.
- The button does nothing. The component has no interactive render mode. Add
@rendermode InteractiveServer. - "Cannot provide a value for property 'Tasks'". You injected a service you never registered. Add it in
Program.cs. - NullReferenceException on first render. Data loaded in
OnInitializedAsyncisn't there yet. Check fornulland show a loading state. - The form submits but nothing validates. You forgot
<DataAnnotationsValidator />inside theEditForm. - Changes don't appear after an external update. If something outside Blazor's event handlers changes state, such as a timer, call
StateHasChanged()(orInvokeAsync(StateHasChanged)from another thread).
When you're stuck, read the browser console and the terminal running dotnet watch. Blazor's error messages are usually specific about what went wrong.
Which render mode should you use?
For this tutorial, Interactive Server is the right choice: it starts fast, keeps code on the server and needs no API layer. As your apps grow, you'll choose per page or per component.

The real decision for most apps is Server vs WebAssembly:

A rule of thumb I give in class: start with Server for internal business apps on a reliable network. Consider WebAssembly or Auto when users are many, spread out, or need parts of the app to keep working with a weak connection.
What should you learn after this tutorial?
In roughly this order:
- Lifecycle methods, especially
OnInitializedAsyncfor loading data. - Calling a real API with
HttpClient. Pair this with our ASP.NET Core Web API tutorial. - A database through EF Core. See EF Core basics with examples.
- Render modes. When to use Interactive WebAssembly or Auto instead of Server. Read how to implement Blazor Web App render modes.
- Authentication with ASP.NET Core Identity and
AuthorizeView.
For a bigger picture of building production apps, our guide on developing robust Blazor web applications is the next read.
Want guided Blazor training?
Our .NET Blazor training takes this further over 40 hours: components, state, APIs, authentication, testing and deployment, with code reviews on every module. You can join live online or in the classroom in Vallabh Vidyanagar, Anand. And if your company needs a Blazor application built, our team does that too: see software and web development.
FAQ
Is Blazor good for beginners?
Yes, if you already know a little C#. You build web pages without learning JavaScript first, and the component model is easy to reason about. Complete programming beginners should learn basic C# before starting Blazor.
Why doesn't my Blazor button do anything?
Most often the component has no interactive render mode. In a Blazor Web App, pages are rendered statically by default. Add @rendermode InteractiveServer at the top of the component, or set interactivity globally when you create the project.
What's the difference between Blazor Server and Blazor WebAssembly?
Interactive Server runs your C# on the server and sends UI updates over SignalR. Interactive WebAssembly downloads the .NET runtime and your code to the browser and runs it there. Server starts faster and keeps code private; WebAssembly works offline and reduces server load. Our comparison post goes deeper.
Which version of .NET should I use for this tutorial?
.NET 10, the current Long Term Support release. The code also works on .NET 8 and on the .NET 11 release, though .NET 8 reaches end of support on 10 November 2026.
Can I use Bootstrap with Blazor?
Yes. The Blazor Web App template already includes Bootstrap, which is why the classes in this tutorial work without setup. You can replace it with any CSS framework or a Blazor component library.