Blazor is a web UI framework from Microsoft that allows developers to build interactive web applications using C#, .NET, and Razor components.
A Blazor component is a reusable part of a user interface that can contain HTML markup, C# logic, data binding, event handlers, and conditional rendering.
Let's understand this with a simple example of a product details component.
<h3>Product Details</h3>
<div class="card shadow-sm p-3">
<h5>@productName</h5>
<p>Price: ₹@price</p>
<button class="btn btn-primary"
@onclick="IncreasePrice">
Increase Price
</button>
</div>
@code {
private string productName = "Wireless Headphones";
private decimal price = 1500;
private void IncreasePrice()
{
price += 100;
}
}
In this example:
- HTML markup defines the structure of the product details.
-
Bootstrap classes such as
card,shadow-sm, andbtn btn-primarystyle the UI. - C# variables store the product name and price.
-
Data binding using
@productNameand@pricedisplays variable values in the UI. -
Event handling using
@onclickcalls theIncreasePrice()method when the button is clicked.
When the user clicks the button, the price increases by ₹100, and Blazor automatically updates the displayed value.
This example demonstrates how Blazor combines C# logic with HTML markup to build interactive user interfaces.
What Is Bootstrap, and Why Use It with Blazor?
Bootstrap is a CSS framework that provides predefined styling classes for layouts, forms, buttons, cards, tables, badges, and responsive designs.
For example, the product component above uses the card class to create a card-style container, shadow-sm to add a subtle shadow, and btn btn-primary to style the button.
Bootstrap handles the visual presentation, while Blazor handles the application logic and user interactions.
Important: Using Bootstrap CSS classes directly in a Razor component is different from using a dedicated Blazor Bootstrap component library. In this article, we use Bootstrap CSS classes with normal Blazor components.
What Are We Going to Build?
Now that we understand how a normal Blazor component works, let's apply the same concepts to a real-world example: a Browse Membership Plans page for a Gym Center and Membership Plan Management System.
The page retrieves membership plans from an ASP.NET Core Web API and displays them using Bootstrap cards.
Users can:
- Browse membership plans for a selected gym.
- Search plans by name.
- View prices and membership durations.
- Check class sessions and personal training sessions.
- View locker and guest access benefits.
- Identify active and inactive plans.
- Navigate to the subscription page.
- Receive loading indicators and error messages.
The following example demonstrates how Blazor components, Bootstrap styling, API integration, and event handling work together.
Complete Browse Membership Plans Component
and place the following code inside it. The complete component is included together below, without changing the supplied code.
@page "/gyms/{GymId:int}/plans"
@using System.Net.Http.Json
@inject HttpClient http
@inject NavigationManager navigation
@inject SweetAlertService Swal
<PageTitle>Browse Membership Plans</PageTitle>
<div class="container-fluid py-4">
<!-- Page Heading -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="fw-bold">Membership Plans</h3>
<p class="text-muted mb-0">
Explore membership plans available at this gym.
</p>
<p class="text-muted small mb-0">
Selected Gym ID: @GymId
</p>
</div>
<button class="btn btn-outline-secondary"
@onclick="GoBack">
Back to Gyms
</button>
</div>
<!-- Search -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<label class="form-label">Search Membership Plan</label>
<input class="form-control"
type="text"
placeholder="Search by plan name..."
@bind="PlanSearch"
@bind:event="oninput" />
</div>
</div>
<!-- Loading -->
@if (isLoading)
{
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="text-muted mt-3">
Loading membership plans...
</p>
</div>
}
else if (hasError)
{
<div class="alert alert-danger">
Unable to load membership plans.
Please try again.
<button class="btn btn-sm btn-outline-danger ms-2"
@onclick="LoadPlans">
Retry
</button>
</div>
}
else if (FilteredPlans.Count == 0)
{
<div class="card shadow-sm">
<div class="card-body text-center py-5">
<h5 class="text-muted">
No membership plans found
</h5>
<p class="text-muted mb-0">
This gym has no matching membership plans.
</p>
</div>
</div>
}
else
{
<!-- Plans List -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">Available Plans</h5>
<span class="badge bg-primary">
@FilteredPlans.Count plans
</span>
</div>
<div class="row g-4">
@foreach (var plan in FilteredPlans)
{
<div class="col-12 col-md-6 col-xl-4" @key="plan.Id">
<div class="card h-100 shadow-sm">
<!-- Plan Header -->
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between
align-items-start gap-2">
<h5 class="fw-bold mb-0">
@plan.PlanName
</h5>
<span class="badge @(IsActive(plan.Status)
? "bg-success"
: "bg-secondary")">
@plan.Status
</span>
</div>
</div>
<!-- Plan Details -->
<div class="card-body">
<div class="mb-3">
<span class="text-muted">Price</span>
<h3 class="text-primary fw-bold mb-0">
₹@plan.Price.ToString("N2")
</h3>
</div>
<p class="mb-2">
<strong>Duration:</strong>
@plan.DurationInMonths months
</p>
<p class="mb-2">
<strong>Class Sessions:</strong>
@plan.ClassSessionsPerWeek per week
</p>
<p class="mb-3">
<strong>Personal Training:</strong>
@plan.PersonalTrainingSessions sessions
</p>
<hr />
<h6 class="fw-semibold mb-3">
Plan Benefits
</h6>
<ul class="list-unstyled mb-0">
<li class="mb-2">
@if (plan.LockerAccess)
{
<span class="text-success me-2">✓</span>
<span>Locker Access</span>
}
else
{
<span class="text-muted me-2">✕</span>
<span class="text-muted">
No Locker Access
</span>
}
</li>
<li class="mb-2">
@if (plan.GuestAccess)
{
<span class="text-success me-2">✓</span>
<span>Guest Access</span>
}
else
{
<span class="text-muted me-2">✕</span>
<span class="text-muted">
No Guest Access
</span>
}
</li>
<li class="mb-2">
<strong>Guest Passes:</strong>
@plan.GuestPasses
</li>
</ul>
</div>
<!-- Subscribe Button -->
<div class="card-footer bg-white border-0 pb-3">
@if (IsActive(plan.Status))
{
<button class="btn btn-primary w-100"
@onclick="() => Subscribe(plan.Id)">
Subscribe
</button>
}
else
{
<button class="btn btn-secondary w-100"
disabled>
Plan Inactive
</button>
}
</div>
</div>
</div>
}
</div>
}
</div>
@code {
// Gym ID comes from the URL:
// /gyms/1/plans means GymId = 1.
[Parameter]
public int GymId { get; set; }
private string PlanSearch = string.Empty;
private bool isLoading = true;
private bool hasError = false;
private List<MembershipPlanItem> planList = new();
// Plans displayed after applying the local search.
private List<MembershipPlanItem> FilteredPlans =>
planList
.Where(plan =>
string.IsNullOrWhiteSpace(PlanSearch) ||
plan.PlanName.Contains(
PlanSearch.Trim(),
StringComparison.OrdinalIgnoreCase))
.ToList();
// Response model used by this page.
public sealed class MembershipPlanItem
{
public int Id { get; set; }
public string PlanName { get; set; } = string.Empty;
public int GymId { get; set; }
public decimal Price { get; set; }
public int DurationInMonths { get; set; }
public int ClassSessionsPerWeek { get; set; }
public int PersonalTrainingSessions { get; set; }
public bool LockerAccess { get; set; }
public bool GuestAccess { get; set; }
public int GuestPasses { get; set; }
public string Status { get; set; } = string.Empty;
}
protected override async Task OnParametersSetAsync()
{
await LoadPlans();
}
private async Task LoadPlans()
{
isLoading = true;
hasError = false;
try
{
// Assumed API endpoint:
// GET api/MembershipPlan/BrowseMembershipPlans?GymId=1
var url =
$"MembershipPlan/BrowseMembershipPlans?GymId={GymId}";
var response = await http.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var result = await response.Content
.ReadFromJsonAsync<List<MembershipPlanItem>>();
// Keep only plans belonging to the selected gym.
// The backend should also enforce this filter.
planList = (result ?? new List<MembershipPlanItem>())
.Where(plan => plan.GymId == GymId)
.ToList();
}
else
{
planList = new();
hasError = true;
await Swal.FireAsync(new SweetAlertOptions
{
Title = "Unable to load plans",
Text = $"The server returned status {(int)response.StatusCode}.",
Icon = SweetAlertIcon.Error
});
}
}
catch (Exception)
{
planList = new();
hasError = true;
await Swal.FireAsync(new SweetAlertOptions
{
Title = "Connection Error",
Text = "Unable to connect to the gym API.",
Icon = SweetAlertIcon.Error
});
}
finally
{
isLoading = false;
}
}
private static bool IsActive(string status)
{
return string.Equals(
status,
"Active",
StringComparison.OrdinalIgnoreCase);
}
private void Subscribe(int planId)
{
// The subscription page will receive both IDs.
navigation.NavigateTo(
$"/subscribe?gymId={GymId}&planId={planId}");
}
private void GoBack()
{
navigation.NavigateTo("/browse-gyms");
}
}
Understanding the Main Concepts
1. Route parameters
The route is defined as:
@page "/gyms/{GymId:int}/plans"
The [Parameter] property receives the gym ID from the URL:
[Parameter]
public int GymId { get; set; }
For example, /gyms/3/plans supplies the value 3 to GymId.
This allows the same component to display plans for different gyms.
2. Data binding and search
The search input uses:
@bind="PlanSearch"
@bind:event="oninput"
The PlanSearch variable is updated as the user types. The FilteredPlans property uses LINQ to filter the loaded plans by name without sending a new API request for each keystroke.
3. Bootstrap cards
The component uses Bootstrap classes such as card, card-header, card-body, and card-footer to organize membership information into cards.
The responsive grid uses col-12 col-md-6 col-xl-4 to display one, two, or three columns depending on the screen width.
4. API integration
The LoadPlans() method sends a GET request to the membership-plan endpoint, deserializes the JSON response into MembershipPlanItem objects, and filters the results for the selected gym.
The backend should also enforce the gym filter. Frontend filtering alone is not a security measure.
5. Conditional rendering
The component displays different UI states depending on isLoading, hasError, and the number of filtered plans.
This provides feedback while data is loading, when an error occurs, and when no matching plans are found.
6. Event handling and navigation
The Subscribe button calls Subscribe(plan.Id), which navigates to the subscription page with the selected gym ID and plan ID.
The Back to Gyms button calls GoBack() to navigate to /browse-gyms.
Conclusion: Understanding Blazor Components Through Practical Examples
Blazor components make it possible to combine UI markup, C# logic, data binding, and event handling in a single reusable unit.
We started with a simple product details component and then applied the same concepts to a real-world membership-plans page.
Consider one final small example that demonstrates how a component can display a membership price and calculate a discounted price:
<h3>Membership Price Calculator</h3>
<p>Original Price: ₹@price</p>
<p>Discount: @discount%</p>
<p>Final Price: ₹@FinalPrice</p>
@code {
private decimal price = 2000;
private decimal discount = 10;
private decimal FinalPrice =>
price - (price * discount / 100);
}
Here, the component uses C# properties and expressions to calculate the final price and display it in the UI. When the underlying values change, Blazor can render the updated result.
The same fundamental approach is used in the Browse Membership Plans component, where data comes from an API and the UI displays prices, benefits, status badges, and subscription actions.
The key takeaway: Blazor manages the component's logic and interactivity, Bootstrap provides the styling, and services such as HttpClient connect the UI to backend data.
These concepts can be applied to many applications, including product catalogs, booking systems, hotel management, employee directories, and subscription platforms.
Thanks for reading! I hope this article helps you understand how normal Blazor components and Bootstrap work together to build practical web applications.
Top comments (0)