DEV Community

Lorenzo_Joaq
Lorenzo_Joaq

Posted on

Understanding Blazor Components with Bootstrap

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!";
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • HTML defines the page structure.
  • Bootstrap classes style the button.
  • @onclick handles the button click.
  • The @code block contains the C# logic.
  • The @message expression 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>
Enter fullscreen mode Exit fullscreen mode

Here:

  • container provides a centered layout with responsive width.
  • mt-4 adds top margin.
  • text-primary applies the primary text color.
  • btn applies Bootstrap button styling.
  • btn-success applies 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++;
    }
}
Enter fullscreen mode Exit fullscreen mode

Part 1: Component directives

@page "/example"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

This part defines what the user sees.

Part 3: C# logic

@code {
    private int count = 0;

    private void Increment()
    {
        count++;
    }
}
Enter fullscreen mode Exit fullscreen mode

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("/");
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

@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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

The form-control class styles text inputs.

<select class="form-select">
    <option value="">All Facilities</option>
</select>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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()">
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

A request could look like:

GymCenter/BrowseGyms?PageNumber=1&PageSize=8&CityName=Delhi
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

The Bootstrap spinner-border class creates a loading animation.

After the request completes, the finally block resets the flag:

finally
{
    isLoading = false;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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("/");
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. The user opens /browse-gyms.
  2. Blazor initializes the component.
  3. OnInitializedAsync() calls DisplayGyms().
  4. The component builds the API query parameters.
  5. HttpClient sends a GET request.
  6. The backend returns gym data.
  7. The response is deserialized into GymListItem objects.
  8. The gymList collection is updated.
  9. QuickGrid displays the records.
  10. Bootstrap styles the inputs, buttons, table, badges, and layout.
  11. 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)