Web UIs with Blazor

Reviewed & published by Brayan K

Blazor lets you build interactive web front-ends in C# instead of JavaScript. By the end of this lesson you'll understand Blazor components in .razor files, Razor syntax, passing data with [Parameter], two-way binding with @bind, event handlers like @onclick, the OnInitializedAsync lifecycle, dependency injection with @inject, routing with @page, the difference between Blazor Server and Blazor WebAssembly, and forms with EditForm validation.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a web page as a wall built from LEGO bricks. Each Blazor component is a brick — a self-contained piece with its own shape (markup) and studs (parameters) that snap into a parent. A [Parameter] is a stud the parent presses data onto; @bind is a two-way connector so a brick and its data move together; an event like @onclick is a little switch on the brick. You assemble a whole page by clicking small, reusable bricks together — and because they're all the same C# you already know, you don't switch languages to do it.

The mental model: components that re-render on state change

A Blazor app is a tree of components. Each component is a .razor file mixing HTML-like markup with C# in a @code block. The component holds state (fields), and when that state changes — typically from an event handler — Blazor automatically re-renders the component and updates only the parts of the page that changed. You describe what the UI should look like for a given state; Blazor handles the DOM.

This is the same component-and-state idea behind every modern front-end framework — here it's just C# all the way down.

📊 Razor Directives & Syntax

SyntaxMeansExample
@pageRoute for the component@page "/counter"
@codeC# logic & state block@code { ... }
[Parameter]Input from a parent[Parameter] public string Name
@bindTwo-way binding<input @bind="name" />
@onclickClick event handler@onclick="Save"
@injectDependency injection@inject HttpClient Http

In Razor, an @ switches from markup into C#: @name prints a value, and @if/@foreach drive the markup.

1. Components, State, and Events

A Blazor component is a .razor file: HTML-like markup at the top, a @code block of C# below. Fields in @code are the component's state, and you display them in markup with @field. Wire a button to a method with @onclick; when the method changes state, Blazor automatically re-renders — no manual DOM updates. Read the classic counter and note how clicking updates the displayed count.

@* Counter.razor — a Blazor component is HTML markup + C# in one file. *@
@* The @page directive makes it routable at /counter. *@
@page "/counter"

<h3>Counter</h3>

<p>Current count: @currentCount</p>

@* @onclick wires the button to the C# method below. After it runs,
   Blazor re-renders the component, so the new count shows on screen. *@
<button @onclick="IncrementCount">Click me</button>

@* The @code block holds the component's C# logic and state. *@
@code {
    // A private field is the component's state.
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;   // change state...
        // ...Blazor automatically re-renders to reflect it. No manual DOM work.
    }
}

2. Two-Way Binding with @bind

@bind connects an input to a C# field in both directions: the input shows the current value, and the user's edits flow straight back into the field. Add @bind:event="oninput" to update on every keystroke rather than when the box loses focus. Because the field is the single source of truth, anything else that displays it updates live too.

@page "/greet"

<h3>Live Greeting</h3>

@* @bind creates TWO-WAY binding: the input shows 'name', and as the
   user types, their edits flow back into the C# 'name' field. *@
<input @bind="name" @bind:event="oninput" placeholder="Your name" />

@* Because the field changed, this line re-renders with the new value. *@
<p>Hello, @name!</p>

@code {
    // The single source of truth for both the input and the greeting.
    private string name = "";
}

Your turn. This toggle just needs its click wired up and the boolean flipped. Fill in the two ___ blanks using the hints.

@page "/toggle"

<h3>Toggle</h3>

@* 🎯 YOUR TURN — fill in the blanks marked with ___ *@

@* 1) Wire this button's click to the Flip method in @code. *@
<button ___="Flip">Flip the switch</button>   @* 👉 the click directive:  @onclick *@

<p>The light is @(isOn ? "ON" : "OFF").</p>

@code {
    private bool isOn = false;

    private void Flip()
    {
        // 2) Toggle the boolean so each click flips ON/OFF.
        isOn = ___;            // 👉 the negation of isOn:  !isOn
    }
}

3. Reusable Components with [Parameter]

Components become reusable when a parent can pass data into them. Mark a public property with [Parameter] and the parent sets it like an HTML attribute: <GreetingCard Name="Ada" />. This is how you build a small component once and render it many times with different data — cards, list rows, badges, and so on.

@* GreetingCard.razor — a reusable child component. *@
@* A parent passes data IN through [Parameter] properties. *@

<div class="card">
    <h4>Hello, @Name!</h4>
    <p>You are visitor number @VisitNumber.</p>
</div>

@code {
    // [Parameter] exposes these so a parent can set them like attributes.
    [Parameter] public string Name { get; set; } = "Guest";
    [Parameter] public int VisitNumber { get; set; }
}

@* A parent renders it like this (illustrative):
   <GreetingCard Name="Ada" VisitNumber="42" />
   which displays: "Hello, Ada!" and "You are visitor number 42." *@

Now you try. Finish this reusable price tag by exposing both fields as parameters so a parent can set them. Fill in the two ___ blanks:

@* PriceTag.razor — finish this reusable child component. *@

<span class="tag">@Label: $@Amount</span>

@code {
    // 🎯 YOUR TURN — fill in the blanks marked with ___

    // 1) Expose 'Label' so a PARENT can set it as an attribute.
    [___] public string Label { get; set; } = "Price";   // 👉 the attribute:  Parameter

    // 2) Expose 'Amount' the same way so the parent can pass a number.
    [Parameter] public decimal ___ { get; set; }          // 👉 the property name:  Amount
}

@* Parent usage:  <PriceTag Label="Total" Amount="19.99" />  *@

4. Dependency Injection and Lifecycle

Components often need services — an HttpClient to call an API, a custom data service. @inject asks the DI container for one and assigns it to a property. To load data, override OnInitializedAsync, which runs once when the component initializes and lets you await the work; when it finishes, Blazor re-renders with the results. Show a "Loading..." placeholder until the data arrives.

@page "/weather"

@* @inject asks the DI container for a registered service and assigns
   it to a property — here an HttpClient for calling an API. *@
@inject HttpClient Http

<h3>Forecast</h3>

@if (forecasts == null)
{
    <p>Loading...</p>           @* shown until the data arrives *@
}
else
{
    <ul>
        @foreach (var f in forecasts)
        {
            <li>@f</li>
        }
    </ul>
}

@code {
    private string[] forecasts;

    // OnInitializedAsync runs once when the component initializes —
    // the right place to AWAIT data loading.
    protected override async Task OnInitializedAsync()
    {
        // Pretend to fetch from an API; here we just simulate a delay.
        await Task.Delay(500);
        forecasts = new[] { "Sunny", "Cloudy", "Rainy" };
        // When the method completes, Blazor re-renders with the data.
    }
}

🔎 Deep Dive: Blazor Server vs Blazor WebAssembly

Blazor runs your components in one of two hosting models, and the component code looks almost identical in both:

// Same component, either host:
//   Server         -> C# executes on the server, UI diffs over SignalR
//   WebAssembly    -> C# executes in the browser (WASM), no round-trips

Because the programming model is shared, you can often start with one model and switch later with minimal changes.

Putting It Together: a Validated Sign-Up Form

Here's a realistic form that ties the lesson together. An EditForm binds to a model; a DataAnnotationsValidator turns [Required] and [EmailAddress] attributes into live validation; InputText uses @bind-Value; and OnValidSubmit only fires once the input is valid. ValidationMessage shows the error next to each field.

@page "/signup"

@* An EditForm bound to a model, validated by data annotations. *@
@using System.ComponentModel.DataAnnotations

<h3>Sign Up</h3>

<EditForm Model="user" OnValidSubmit="HandleSubmit">
    @* Turns [Required]/[EmailAddress] attributes into live validation. *@
    <DataAnnotationsValidator />

    <label>Name:</label>
    <InputText @bind-Value="user.Name" />
    <ValidationMessage For="@(() => user.Name)" />

    <label>Email:</label>
    <InputText @bind-Value="user.Email" />
    <ValidationMessage For="@(() => user.Email)" />

    <button type="submit">Register</button>
</EditForm>

<p>@status</p>

@code {
    private Account user = new();
    private string status = "";

    // OnValidSubmit fires ONLY when validation passes.
    private void HandleSubmit() => status = "Welcome, " + user.Name + "!";

    public class Account
    {
        [Required] public string Name { get; set; } = "";
        [Required, EmailAddress] public string Email { get; set; } = "";
    }
}

Validation, binding, and submission are all C# — the same skills from the rest of the course, now driving a web form.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Make a route@page "/path"Top of the .razor file
Show a value@count@ switches to C#
Handle a click@onclick="DoThing"Re-renders after
Two-way bind@bind="name"Input ↔ field
Accept input[Parameter] public ...From a parent
Inject a service@inject HttpClient HttpFrom DI
Load on initOnInitializedAsync()await data here
Validated form<EditForm Model="m">+ DataAnnotationsValidator

Frequently Asked Questions

Q: Does Blazor replace JavaScript entirely?

For most UI logic, yes — you write components in C#. You can still call JavaScript when you need a browser API or a JS library (via JS interop), but day-to-day interactivity is C#.

Q: Should I choose Blazor Server or WebAssembly?

Server gives a tiny initial download and direct server access but needs a live SignalR connection. WebAssembly runs in the browser, works offline, and feels instant after a larger first load. The component code is nearly identical either way.

Q: What's the difference between @bind and @onclick?

@bind creates a two-way data link between an input and a C# value. @onclick (and other @on... handlers) runs a C# method in response to an event, after which the component re-renders.

Q: Why use OnInitializedAsync instead of the constructor for loading data?

The constructor can't await, and parameters aren't set yet. OnInitializedAsync runs after the component is set up, can await async calls, and triggers a re-render when the data is ready.

Mini-Challenge: A Live Tip Calculator

No blanks this time — just a brief and an outline. Build a component with bound bill and tipPercent inputs and a computed Tip property that updates live as the user types. For a bonus, also show the grand total. Run it and watch the tip recalculate on every keystroke.

@page "/tip"

<h3>Tip Calculator</h3>

@* 🎯 MINI-CHALLENGE: a live tip calculator component
   1. Add two fields in @code: 'bill' (decimal) and 'tipPercent' (int, e.g. 15).
   2. Two-way bind a number <input> to each using @bind, so editing them
      updates the C# fields.
   3. Add a computed property:  decimal Tip => bill * tipPercent / 100m;
   4. Display the tip with  <p>Tip: $@Tip</p>  so it updates as the user types.
   5. BONUS: also show the grand total (bill + Tip).

   ✅ Expected: bill 50 and tipPercent 20 shows "Tip: $10" live, updating
      the instant either input changes. *@

@code {
    // your code here
}

🎉 Lesson Complete

Practice quiz

What does Blazor let you build that previously required JavaScript?

  • Interactive web user interfaces written in C#
  • Mobile device drivers
  • SQL database engines
  • Operating system kernels

Answer: Interactive web user interfaces written in C#. Blazor lets you build interactive web UIs using C# and Razor components instead of writing the UI logic in JavaScript.

What file extension do Blazor components use?

  • .xaml
  • .cshtml
  • .razor
  • .blazor

Answer: .razor. Blazor components are written in .razor files, which combine Razor markup (HTML plus C#) with component logic.

Which attribute marks a C# property as a component parameter that a parent can set?

  • [Parameter]
  • [Inject]
  • [Bind]
  • [Route]

Answer: [Parameter]. The [Parameter] attribute exposes a public property so a parent component can pass a value into the child, like an attribute in markup.

In Razor markup, how do you wire a button click to a C# method named Increment?

  • click={Increment}
  • @onclick="Increment"
  • bind:click=Increment
  • onclick=Increment

Answer: @onclick="Increment". Razor event handlers use the @on prefix; @onclick="Increment" calls the C# method when the button is clicked.

What does @bind do on an input element?

  • Sends the value to a database
  • Deletes the element
  • Adds CSS classes only
  • Creates a two-way binding between the input and a C# field/property

Answer: Creates a two-way binding between the input and a C# field/property. @bind sets up two-way data binding: the input shows the C# value and writes user edits back into that field or property.

Which lifecycle method is the right place to load data asynchronously when a component initializes?

  • OnInitializedAsync()
  • Render()
  • Dispose()
  • Main()

Answer: OnInitializedAsync(). OnInitializedAsync() runs when the component is first initialized and is the standard place to await data loading such as an API call.

How do you request a service via dependency injection inside a .razor component?

  • @page the service
  • @inject SomeService Name
  • @using the service
  • new Service()

Answer: @inject SomeService Name. The @inject directive asks the DI container for a service and assigns it to a property, e.g. @inject HttpClient Http.

What is the key difference between Blazor Server and Blazor WebAssembly?

  • Server uses C# while WebAssembly uses Java
  • WebAssembly cannot render HTML
  • There is no difference
  • Server runs C# on the server with UI updates over a SignalR connection; WebAssembly runs the .NET code in the browser

Answer: Server runs C# on the server with UI updates over a SignalR connection; WebAssembly runs the .NET code in the browser. Blazor Server executes component logic on the server and pushes UI diffs to the browser over SignalR; Blazor WebAssembly downloads a .NET runtime and runs the C# directly in the browser.

Which directive at the top of a component defines its route, e.g. /counter?

  • @route
  • @url
  • @page "/counter"
  • @map

Answer: @page "/counter". @page "/counter" sets the route template so navigating to /counter renders that routable component.

What does an EditForm with DataAnnotationsValidator provide?

  • Automatic database migrations
  • Model-based form validation that shows messages when input is invalid
  • Image compression
  • Server logging only

Answer: Model-based form validation that shows messages when input is invalid. EditForm binds to a model and, with DataAnnotationsValidator, validates it against data-annotation attributes, surfacing validation messages in the UI.

Continue this course