Understanding Blazor Components with Bootstrap
Introduction to Blazor Components and Bootstrap
Blazor is a web UI framework from Microsoft that allows developers to build interactive web applications using C#, Razor syntax, and .NET instead of relying entirely on JavaScript.
One of the most important concepts in Blazor is the component.
A Blazor component is a reusable piece of UI that can contain HTML markup, C# logic, data, event handlers, and styling. Components are generally written in .razor files.
For example, a simple Blazor component can display a heading and a button:
<h3>Welcome to My Application</h3>
<button class="btn btn-primary" @onclick="ShowMessage">
Click Me
</button>
<p>@message</p>
@code {
private string message = string.Empty;
private void ShowMessage()
{
message = "Welcome to Blazor!";
}
}
In this example:
- HTML defines the page structure.
- Bootstrap classes style the button.
-
@onclickhandles the button click. - The
@codeblock contains the C# logic. - The
@messageexpression displays a C# variable in the UI.
When the user clicks the button, the ShowMessage() method executes, updates the message, and Blazor automatically renders the updated UI.
This is one of the main advantages of Blazor: developers can combine UI markup and C# logic in a component without manually updating the DOM.
What Is Bootstrap, and Why Use It with Blazor?
Bootstrap is a popular CSS framework that provides predefined styles, layout utilities, responsive grids, buttons, forms, tables, badges, and other UI patterns.
When Bootstrap is used in a Blazor application, its CSS classes can be applied directly to HTML elements inside Razor components.
For example:
<div class="container mt-4">
<h3 class="text-primary">Browse Gyms</h3>
<button class="btn btn-success">
View Details
</button>
</div>
Here:
-
containerprovides a centered layout with responsive width. -
mt-4adds top margin. -
text-primaryapplies the primary text color. -
btnapplies Bootstrap button styling. -
btn-successapplies the success button appearance.
Bootstrap handles the visual styling, while Blazor handles the application logic and interactivity.
Important: Bootstrap CSS classes do not automatically create Blazor components. For example, class="btn btn-primary" styles an HTML button, while @onclick connects that button to a Blazor event handler.
We will use the following technologies:
| Technology | Purpose |
|---|---|
| Blazor WebAssembly | Builds the interactive frontend. |
| Razor components | Combine HTML markup and C# logic. |
| Bootstrap | Styles buttons, forms, tables, badges, and layouts. |
| QuickGrid | Displays and sorts gym records in a table. |
| HttpClient | Sends HTTP requests to the backend API. |
| ASP.NET Core Web API | Provides gym data. |
| SweetAlert | Displays error alerts. |
| NavigationManager | Navigates between application routes. |
1. Understanding the Structure of a Blazor Component
A typical Razor component can contain three main parts:
@page "/example"
<h3>Example Component</h3>
<button class="btn btn-primary" @onclick="Increment">
Click Me
</button>
<p>Count: @count</p>
@code {
private int count = 0;
private void Increment()
{
count++;
}
}
Part 1: Component directives
@page "/example"
The @page directive defines the route used to access the component.
Part 2: UI markup
<h3>Example Component</h3>
<button class="btn btn-primary" @onclick="Increment">
Click Me
</button>
<p>Count: @count</p>
This part defines what the user sees.
Part 3: C# logic
@code {
private int count = 0;
private void Increment()
{
count++;
}
}
The @code block contains variables and methods used by the component.
This same structure is used in the Browse Gyms component. The difference is that the component contains API integration, search filters, pagination, and a data grid.
2. Complete Browse Gyms Component
The following is the complete Razor component used for the example. It includes the markup, Bootstrap styling, QuickGrid columns, model class, API request logic, filtering, pagination, and navigation.
Create a Razor component named BrowseGyms.razor and place the code in it. Ensure the required QuickGrid and SweetAlert dependencies are installed and configured in your project.
@page "/browse-gyms"
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.QuickGrid
@inject HttpClient http
@inject NavigationManager navigation
@inject SweetAlertService Swal
<PageTitle>Browse Gyms</PageTitle>
<div class="d-flex justify-content-between align-items-center m-2 p-2">
<div>
<h3>Browse Gyms</h3>
<p class="text-muted mb-0">
Search gyms and explore their membership plans.
</p>
</div>
<button class="btn btn-outline-secondary"
@onclick="GoBack">
Back
</button>
</div>
<div class="m-3 rounded border p-3 shadow-sm">
@if (isLoading)
{
<div class="d-flex justify-content-center p-5">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else
{
<!-- Search and Filters -->
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Search Gym</label>
<input class="form-control"
type="text"
placeholder="Search gym name or address..."
@bind="NameFilter"
@bind:event="oninput" />
</div>
<div class="col-md-3">
<label class="form-label">City</label>
<input class="form-control"
type="text"
placeholder="Enter city..."
@bind="CityFilter"
@bind:event="oninput" />
</div>
<div class="col-md-2">
<label class="form-label">Facility</label>
<select class="form-select"
@bind="FacilityFilter">
<option value="">All Facilities</option>
<option value="Locker">Locker</option>
<option value="Parking">Parking</option>
<option value="Both">Locker and Parking</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Opening Hours</label>
<select class="form-select"
@bind="OpeningHoursFilter">
<option value="">Any Opening Hours</option>
<option value="Morning">Opens before 9 AM</option>
<option value="Evening">Closes at or after 8 PM</option>
</select>
</div>
<div class="col-12 d-flex gap-2">
<button class="btn btn-primary"
@onclick="SearchGyms">
Search
</button>
<button class="btn btn-outline-secondary"
@onclick="ResetFilters">
Clear Filters
</button>
</div>
</div>
@if (gymList.Count > 0)
{
<!-- QuickGrid -->
<div class="table-responsive">
<QuickGrid TGridItem="GymListItem"
Class="table table-hover table-bordered table-striped align-middle"
Items="@gymList.AsQueryable()">
<PropertyColumn Property="g => g.GymName"
Title="Gym Name"
Sortable="true" />
<PropertyColumn Property="g => g.CityName"
Title="City"
Sortable="true" />
<PropertyColumn Property="g => g.Address"
Title="Address" />
<PropertyColumn Property="g => g.Phone"
Title="Phone" />
<!-- Opening Hours -->
<TemplateColumn Title="Opening Hours"
Context="gym">
@gym.OpeningTime.ToString(@"hh\:mm")
-
@gym.ClosingTime.ToString(@"hh\:mm")
</TemplateColumn>
<!-- Facilities -->
<TemplateColumn Title="Facilities"
Context="gym">
@if (gym.LockerFacility)
{
<span class="badge bg-info me-1">
Locker
</span>
}
@if (gym.ParkingAvailable)
{
<span class="badge bg-secondary">
Parking
</span>
}
@if (!gym.LockerFacility &&
!gym.ParkingAvailable)
{
<span class="text-muted">None</span>
}
</TemplateColumn>
<!-- Status -->
<TemplateColumn Title="Status"
Context="gym">
<span class="badge @(string.Equals(
gym.Status,
"Active",
StringComparison.OrdinalIgnoreCase)
? "bg-success"
: "bg-secondary")">
@gym.Status
</span>
</TemplateColumn>
<!-- Actions -->
<TemplateColumn Title="Action"
Context="gym">
<button class="btn btn-sm btn-primary"
disabled="@(!string.Equals(
gym.Status,
"Active",
StringComparison.OrdinalIgnoreCase))"
@onclick="() => ViewPlans(gym.Id)">
View Plans
</button>
</TemplateColumn>
</QuickGrid>
</div>
}
else
{
<div class="p-4 text-center">
<h5 class="text-muted">No gyms found</h5>
<p class="text-muted mb-0">
Try changing your search or filter criteria.
</p>
</div>
}
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-3">
<span class="text-muted">
Page @pageNumber
</span>
<div>
<button class="btn btn-outline-primary me-2"
disabled="@(pageNumber <= 1)"
@onclick="Prev">
Previous
</button>
<button class="btn btn-outline-primary"
disabled="@(!canGoNext)"
@onclick="Next">
Next
</button>
</div>
</div>
}
</div>
@code {
private int pageNumber = 1;
private const int pageSize = 8;
private string NameFilter = string.Empty;
private string CityFilter = string.Empty;
private string FacilityFilter = string.Empty;
private string OpeningHoursFilter = string.Empty;
private bool isLoading = false;
private bool canGoNext = false;
private List<GymListItem> gymList = new();
// Model used by this page to read gym data from the API.
public sealed class GymListItem
{
public int Id { get; set; }
public string GymName { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string CityName { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public TimeSpan OpeningTime { get; set; }
public TimeSpan ClosingTime { get; set; }
public int TotalClassCapacity { get; set; }
public bool LockerFacility { get; set; }
public bool ParkingAvailable { get; set; }
public string Status { get; set; } = string.Empty;
}
protected override async Task OnInitializedAsync()
{
await DisplayGyms();
}
private async Task DisplayGyms()
{
if (isLoading)
{
return;
}
isLoading = true;
try
{
var query = new List<string>
{
$"PageNumber={pageNumber}",
$"PageSize={pageSize}"
};
if (!string.IsNullOrWhiteSpace(NameFilter))
{
query.Add(
$"Search={Uri.EscapeDataString(NameFilter.Trim())}");
}
if (!string.IsNullOrWhiteSpace(CityFilter))
{
query.Add(
$"CityName={Uri.EscapeDataString(CityFilter.Trim())}");
}
if (!string.IsNullOrWhiteSpace(FacilityFilter))
{
query.Add(
$"Facility={Uri.EscapeDataString(FacilityFilter)}");
}
if (!string.IsNullOrWhiteSpace(OpeningHoursFilter))
{
query.Add(
$"OpeningHours={Uri.EscapeDataString(OpeningHoursFilter)}");
}
var url =
"GymCenter/BrowseGyms?" + string.Join("&", query);
var response = await http.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var result = await response.Content
.ReadFromJsonAsync<List<GymListItem>>();
gymList = result ?? new List<GymListItem>();
// A full page means another page might exist.
canGoNext = gymList.Count == pageSize;
}
else
{
gymList = new();
canGoNext = false;
await Swal.FireAsync(new SweetAlertOptions
{
Title = "Unable to load gyms",
Text = $"The server returned status {(int)response.StatusCode}.",
Icon = SweetAlertIcon.Error
});
}
}
catch (Exception)
{
gymList = new();
canGoNext = false;
await Swal.FireAsync(new SweetAlertOptions
{
Title = "Connection Error",
Text = "Unable to connect to the gym API. Check that the backend is running.",
Icon = SweetAlertIcon.Error
});
}
finally
{
isLoading = false;
}
}
private async Task SearchGyms()
{
pageNumber = 1;
await DisplayGyms();
}
private async Task ResetFilters()
{
NameFilter = string.Empty;
CityFilter = string.Empty;
FacilityFilter = string.Empty;
OpeningHoursFilter = string.Empty;
pageNumber = 1;
await DisplayGyms();
}
private async Task Next()
{
if (!canGoNext || isLoading)
{
return;
}
pageNumber++;
await DisplayGyms();
}
private async Task Prev()
{
if (pageNumber <= 1 || isLoading)
{
return;
}
pageNumber--;
await DisplayGyms();
}
private void ViewPlans(int gymId)
{
navigation.NavigateTo($"/gyms/{gymId}/plans");
}
private void GoBack()
{
navigation.NavigateTo("/");
}
}
3. Understanding the Component Directives
At the beginning of the component, we have:
@page "/browse-gyms"
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.QuickGrid
@inject HttpClient http
@inject NavigationManager navigation
@inject SweetAlertService Swal
@page
The @page directive defines the route of the component.
When a user opens /browse-gyms, Blazor renders this page.
@using
The @using directive imports namespaces so that types and extension methods can be used without writing their fully qualified names.
@inject
The @inject directive requests services through dependency injection.
In this example, HttpClient retrieves data, NavigationManager handles navigation, and SweetAlertService displays alerts.
These directives illustrate how a normal Blazor component can connect its UI with application services.
4. Understanding Bootstrap Classes in the Component
Bootstrap classes are used throughout the component to create a clean, responsive interface.
For example:
<div class="m-3 rounded border p-3 shadow-sm">
<h3>Browse Gyms</h3>
</div>
The classes provide:
-
m-3: margin. -
rounded: rounded corners. -
border: a border around the element. -
p-3: padding. -
shadow-sm: a subtle shadow.
Responsive layout
<div class="row g-3 mb-3">
<div class="col-md-4">
...
</div>
<div class="col-md-3">
...
</div>
</div>
Bootstrap's grid system organizes the search inputs into responsive columns.
The col-md-* classes define column widths at medium breakpoints and above. On smaller screens, the columns can stack vertically.
Buttons
<button class="btn btn-primary">
Search
</button>
The btn class provides the standard Bootstrap button styling, while btn-primary applies the primary color.
Similarly, btn-outline-secondary creates an outlined secondary button, and btn-sm makes a button smaller.
Form controls
<input class="form-control" />
The form-control class styles text inputs.
<select class="form-select">
<option value="">All Facilities</option>
</select>
The form-select class styles dropdown controls.
These classes help maintain a consistent appearance across the page.
5. Understanding Data Binding and Event Handling
One of the most important features of Blazor is data binding.
Consider this input:
<input class="form-control"
@bind="NameFilter"
@bind:event="oninput" />
The @bind directive connects the input to the NameFilter variable.
When the user types, the variable is updated through the oninput event.
However, typing does not immediately send an API request in this implementation. The request occurs when the user clicks the Search button.
The button uses:
<button class="btn btn-primary"
@onclick="SearchGyms">
Search
</button>
Here, @onclick connects the button click to the SearchGyms() method.
This demonstrates the relationship between UI events and C# methods in a normal Blazor component.
6. Understanding the Gym Model
Inside the @code block, the component defines GymListItem.
This class represents the shape of the gym records expected from the backend API.
public sealed class GymListItem
{
public int Id { get; set; }
public string GymName { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string CityName { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public TimeSpan OpeningTime { get; set; }
public TimeSpan ClosingTime { get; set; }
public int TotalClassCapacity { get; set; }
public bool LockerFacility { get; set; }
public bool ParkingAvailable { get; set; }
public string Status { get; set; } = string.Empty;
}
The class is used when converting the API's JSON response into C# objects.
For example, LockerFacility and ParkingAvailable are Boolean properties. The component uses these values to decide which facility badges should be displayed.
The model can remain inside the component for this example. In a larger application, it may be preferable to move shared response models into a separate file.
7. Understanding the QuickGrid Component
QuickGrid is a component used to display tabular data.
In the example:
<QuickGrid TGridItem="GymListItem"
Class="table table-hover table-bordered table-striped align-middle"
Items="@gymList.AsQueryable()">
TGridItem defines the type of each row.
Items provides the data collection.
Class applies Bootstrap table styles.
The following column displays the gym name:
<PropertyColumn Property="g => g.GymName"
Title="Gym Name"
Sortable="true" />
The lambda expression g => g.GymName specifies which property to display. The Title defines the column heading, and Sortable="true" enables sorting.
For custom displays, the component uses TemplateColumn.
For example:
<TemplateColumn Title="Facilities"
Context="gym">
@if (gym.LockerFacility)
{
<span class="badge bg-info me-1">
Locker
</span>
}
</TemplateColumn>
Context="gym" names the current row's object inside the template.
This allows the component to use conditions and custom markup for each row.
Notice that QuickGrid and Bootstrap have different responsibilities: QuickGrid renders the grid structure and handles supported grid features, while Bootstrap provides the visual styling.
8. Understanding API Integration
The component loads data in OnInitializedAsync():
protected override async Task OnInitializedAsync()
{
await DisplayGyms();
}
This lifecycle method runs when the component initializes.
The DisplayGyms() method constructs the query parameters and sends an HTTP GET request.
var url =
"GymCenter/BrowseGyms?" + string.Join("&", query);
var response = await http.GetAsync(url);
A request could look like:
GymCenter/BrowseGyms?PageNumber=1&PageSize=8&CityName=Delhi
The actual URL depends on which filters have been selected.
The backend should accept these query parameters and apply the corresponding search, filtering, and pagination logic.
If the request succeeds, the component deserializes the response:
var result = await response.Content
.ReadFromJsonAsync<List<GymListItem>>();
gymList = result ?? new List<GymListItem>();
The resulting list becomes the data source for QuickGrid.
This is a common pattern in Blazor applications: call an API, convert the response to a C# model, update component state, and render the result.
9. Understanding Loading and Error Handling
The component uses:
private bool isLoading = false;
Before making the request, DisplayGyms() sets the flag to true.
The markup checks that flag:
@if (isLoading)
{
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
}
The Bootstrap spinner-border class creates a loading animation.
After the request completes, the finally block resets the flag:
finally
{
isLoading = false;
}
If the server returns an unsuccessful response, the component uses SweetAlert to display an error message. Exceptions are also handled in the catch block.
In a production application, logging exceptions and returning more specific error messages can make troubleshooting easier.
10. Understanding Pagination
The component defines:
private int pageNumber = 1;
private const int pageSize = 8;
The page number identifies the requested page, and the page size determines how many records the frontend requests per page.
When the user clicks Next:
private async Task Next()
{
if (!canGoNext || isLoading)
{
return;
}
pageNumber++;
await DisplayGyms();
}
The method checks whether another page might exist and whether a request is already in progress. If navigation is allowed, it increments the page number and fetches the next page.
The Previous method works similarly but prevents the page number from going below 1.
The component estimates whether another page exists using:
canGoNext = gymList.Count == pageSize;
This works as an approximation, but it is not guaranteed to be correct if the backend returns a full page that is also the final page. For reliable pagination, the API should return metadata such as HasNextPage or TotalCount.
11. Understanding Navigation
The component uses NavigationManager to navigate to other routes.
private void ViewPlans(int gymId)
{
navigation.NavigateTo($"/gyms/{gymId}/plans");
}
When the user clicks View Plans, the selected gym's ID is inserted into the destination URL.
For example, if the gym ID is 5, the destination becomes /gyms/5/plans.
The Back button calls:
private void GoBack()
{
navigation.NavigateTo("/");
}
This explicitly navigates to the root route of the application.
The View Plans button is disabled for gyms that are not Active. This improves the user experience, but the backend must also enforce relevant business rules.
12. Complete Execution Flow
The component follows this sequence:
- The user opens
/browse-gyms. - Blazor initializes the component.
-
OnInitializedAsync()callsDisplayGyms(). - The component builds the API query parameters.
-
HttpClientsends a GET request. - The backend returns gym data.
- The response is deserialized into
GymListItemobjects. - The
gymListcollection is updated. - QuickGrid displays the records.
- Bootstrap styles the inputs, buttons, table, badges, and layout.
- User actions trigger the search, reset, pagination, or navigation methods.
This example demonstrates how a normal Blazor component combines markup, state, event handling, API calls, and CSS styling to implement a practical user interface.
Conclusion: Building Better Blazor Components with Bootstrap
Blazor components provide a way to organize an application's UI and behavior into manageable, reusable pieces. Bootstrap complements Blazor by providing responsive styling and ready-made CSS classes for layouts, buttons, forms, and tables.
In this example, we built a Browse Gyms page that combines:
- Razor markup and C# logic.
- Bootstrap CSS classes for responsive styling.
- QuickGrid for tabular data.
- HttpClient for backend API integration.
- Data binding and event handlers for user interaction.
- Pagination, navigation, loading indicators, and error handling.
The key takeaway is that Blazor handles the component logic and interactivity, while Bootstrap handles the visual styling. Additional components such as QuickGrid provide specialized functionality that can be combined with both.
Once these concepts are understood, the same approach can be used to build many other application screens, including hotel listings, product catalogs, employee directories, and booking management pages.
By learning how normal Blazor components work together with Bootstrap, developers can create interfaces that are easier to maintain, more responsive, and more consistent across an application.
Thanks for reading! I hope this article helps you understand how Blazor components and Bootstrap can work together in a real-world project.
Top comments (0)