Home C# / .NET Blazor Basics: Build Interactive Web UIs with C# Instead of JavaScript

Blazor Basics: Build Interactive Web UIs with C# Instead of JavaScript

In Plain English 🔥
Imagine your TV remote. Normally you need a special remote (JavaScript) to change the channel on a specific TV (the browser). Blazor is like a universal remote that lets you use the same language you already know (C#) to control the browser directly. You stop learning two different languages and just use the one you love — C# does the clicking, the form-filling, and the live updates, all without writing a single line of JavaScript.
⚡ Quick Answer
Imagine your TV remote. Normally you need a special remote (JavaScript) to change the channel on a specific TV (the browser). Blazor is like a universal remote that lets you use the same language you already know (C#) to control the browser directly. You stop learning two different languages and just use the one you love — C# does the clicking, the form-filling, and the live updates, all without writing a single line of JavaScript.

For years, .NET developers had an invisible wall between their backend C# code and the interactive bits of a web page. The moment a button needed to respond to a click, or a form needed real-time validation, you had to context-switch into JavaScript. That wall created bugs at the seam, slowed teams down, and meant your carefully typed C# models had to be reimplemented as loosely-typed JavaScript objects. Blazor tears that wall down completely.

Blazor is Microsoft's answer to the question: 'What if the entire web stack — UI logic, state management, API calls, and validation — could be written in C#?' It achieves this either by running your .NET code directly in the browser via WebAssembly, or by keeping your code on the server and streaming UI updates over a persistent SignalR connection. Either way, your Razor components are the building blocks: self-contained files that mix C# logic with HTML markup, re-render automatically when data changes, and compose together exactly like React or Vue components — but in the language .NET developers already know cold.

By the end of this article you'll understand the difference between Blazor Server and Blazor WebAssembly (and when to pick each), how the component lifecycle controls rendering, how two-way data binding keeps your UI and your model in sync, and how to wire up real event handling. Every concept comes with a complete, runnable example so you can paste it into a new project and see it work immediately.

Blazor Components — The Single File That Does Everything

In Blazor, a component is a .razor file that contains three things in one place: HTML markup that defines the structure, a @code block that holds C# logic, and optional CSS isolation. This is the core mental model shift — stop thinking in terms of separate HTML files, JavaScript files, and C# controllers. A Blazor component is a self-contained unit of UI.

Every component has a lifecycle. When Blazor first renders a component it calls OnInitializedAsync — this is where you load data from an API or a database. After every parameter change, OnParametersSetAsync fires. The framework tracks which properties are bound to the UI and re-renders only the affected DOM nodes when those values change.

Components are also reusable by design. You define a ProductCard component once and drop anywhere in your app. Parameters flow down from parent to child via [Parameter] attributes, keeping your UI predictable and testable.

The key reason Blazor components beat raw Razor Pages for interactive UIs is that a Razor Page requires a full HTTP round-trip to reflect any change. A Blazor component updates in-place, in memory, without reloading the page — just like a React component does, but entirely in C#.

ProductCard.razor · CSHARP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
@* ProductCard.razor — a reusable component that displays one product *@
@* Drop it anywhere with: <ProductCard Product="someProduct" OnAddToCart="HandleAdd" /> *@

<div class="product-card">
    <h3>@Product.Name</h3>
    <p class="price">@Product.Price.ToString("C")</p>   @* 'C' formats as currency *@

    @if (Product.StockCount > 0)
    {
        <!-- Button is only rendered when stock exists -->
        <button class="btn-primary" @onclick="AddToCart">Add to Cart</button>
    }
    else
    {
        <span class="out-of-stock">Out of Stock</span>
    }
</div>

@code {
    // [Parameter] marks a public property that a parent component can set
    [Parameter]
    public required ProductModel Product { get; set; }

    // EventCallback lets the child notify the parent without tight coupling
    [Parameter]
    public EventCallback<ProductModel> OnAddToCart { get; set; }

    // This method is called when the button fires its @onclick event
    private async Task AddToCart()
    {
        // Invoke the parent's handler and pass the product as the argument
        await OnAddToCart.InvokeAsync(Product);
    }

    // Lifecycle hook — runs once after the component first renders
    protected override void OnInitialized()
    {
        // Good place for lightweight sync setup (no data fetching here)
        Console.WriteLine($"ProductCard initialized for: {Product.Name}");
    }
}

// --- ProductModel.cs (put this in your Models folder) ---
public class ProductModel
{
    public int    Id         { get; set; }
    public string Name       { get; set; } = string.Empty;
    public decimal Price     { get; set; }
    public int    StockCount { get; set; }
}
▶ Output
Console (browser dev tools or terminal for Blazor Server):
ProductCard initialized for: Mechanical Keyboard

Rendered HTML (when StockCount > 0):
<div class="product-card">
<h3>Mechanical Keyboard</h3>
<p class="price">$129.99</p>
<button class="btn-primary">Add to Cart</button>
</div>
⚠️
Pro Tip: EventCallback vs ActionAlways use EventCallback instead of Action for component events. EventCallback automatically calls StateHasChanged on the parent after the handler runs, so the parent's UI updates without you lifting a finger. Action skips that step and your UI appears to ignore the event.

Two-Way Data Binding and Real-Time Form State with @bind

One of the first things you'll want to do in any web app is connect a form field to a C# object and have changes reflect immediately — in both directions. Type in the box, the C# property updates. Change the C# property in code, the box updates. Blazor calls this two-way binding and it's powered by the @bind directive.

Under the hood, @bind on an is syntactic sugar for wiring up the value attribute to a property AND attaching an onchange event handler that writes the new value back. You can be explicit about timing using @bind:event="oninput" if you want the property to update on every keystroke rather than on blur.

This matters for real-world scenarios like live search boxes, character counters, or dependent dropdowns where changing one field should immediately filter the options in another. Without two-way binding you'd write the same plumbing logic yourself every time.

Beyond simple inputs, EditForm is Blazor's built-in form component that wraps DataAnnotations validation. You get client-side validation for free from the same [Required] and [Range] attributes you already use on your models — no duplicate validation rules in JavaScript.

CheckoutForm.razor · CSHARP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
@page "/checkout"
@* This page demonstrates two-way binding, live character count, and form validation *@

<h2>Checkout</h2>

@* EditForm links the form to the C# model and handles validation lifecycle *@
<EditForm Model="order" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />   @* Reads [Required], [Range] etc. from OrderModel *@
    <ValidationSummary />          @* Displays all current validation errors *@

    <div class="form-group">
        <label>Full Name</label>
        @* @bind wires value <-> order.CustomerName two-ways *@
        <InputText @bind-Value="order.CustomerName" class="form-control" />
        <ValidationMessage For="@(() => order.CustomerName)" />
    </div>

    <div class="form-group">
        <label>Order Notes (@notesRemaining chars remaining)</label>
        @* @bind:event="oninput" updates on EVERY keystroke, not just on blur *@
        <textarea @bind="order.Notes" @bind:event="oninput"
                  class="form-control" maxlength="200"></textarea>
    </div>

    <div class="form-group">
        <label>Quantity</label>
        <InputNumber @bind-Value="order.Quantity" class="form-control" />
        <ValidationMessage For="@(() => order.Quantity)" />
    </div>

    <button type="submit" class="btn-primary">Place Order</button>
</EditForm>

@if (orderConfirmed)
{
    <p class="success">Order placed for @order.CustomerName! Qty: @order.Quantity</p>
}

@code {
    // The model the form is bound to — all fields live here
    private OrderModel order = new();
    private bool orderConfirmed = false;

    // Computed property — recalculates every render cycle automatically
    private int notesRemaining => 200 - (order.Notes?.Length ?? 0);

    // OnValidSubmit only fires when ALL DataAnnotations pass
    private async Task HandleValidSubmit()
    {
        // In a real app you'd call an API here
        await Task.Delay(500);  // Simulate a network call
        orderConfirmed = true;
    }
}

// --- OrderModel.cs ---
using System.ComponentModel.DataAnnotations;

public class OrderModel
{
    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, ErrorMessage = "Name too long")]
    public string CustomerName { get; set; } = string.Empty;

    public string? Notes { get; set; }

    [Range(1, 99, ErrorMessage = "Quantity must be between 1 and 99")]
    public int Quantity { get; set; } = 1;
}
▶ Output
When user types in Name field and clicks Place Order with empty name:
Name is required

When form is valid and submitted:
Order placed for Jane Smith! Qty: 3

As user types in Notes textarea (live):
Order Notes (183 chars remaining) <- updates on every keystroke
⚠️
Watch Out: @bind vs @bind-ValueUse @bind on native HTML elements like and