This is the final post in the series. The Task Tracker has been built, refactored, rendered, and made interactive, genuinely working since Part 4, but visually plain, using nothing but default, unstyled HTML elements. This post applies Bootstrap directly on top of that same working Blazor app, with zero changes to the underlying logic, purely visual, closing out the series with something that actually looks presentable.
What Bootstrap Actually Is
Bootstrap is a CSS framework, a large, pre-written collection of CSS classes covering layout, spacing, colors, and common UI components, buttons, cards, navigation bars, so a page can look genuinely designed without writing custom CSS from scratch.
Think of pre-fabricated furniture versus building everything from raw lumber. Bootstrap's classes are like a well-designed furniture catalog, a specific bookshelf, a specific desk, already proportioned and finished correctly. You still arrange the room yourself, the actual layout of your page, but you're not hand-cutting and sanding every piece of wood from scratch.
Getting Bootstrap Into the Project
<!-- In the main HTML host page (index.html for Blazor
WebAssembly, or _Layout.cshtml/App.razor's host
page for Blazor Server) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<!-- That single line makes every Bootstrap class used
throughout this post immediately available across
the entire Blazor app -->
The Grid System: Rows and Columns, No Custom CSS
Bootstrap's layout is based on a 12-column grid. A row contains one or more columns, and the column widths, col-md-8, col-md-4, should add up to 12 for a clean, full-width layout.
<div class="row">
<div class="col-md-8">
<!-- Main content - takes 8 of 12 columns -->
</div>
<div class="col-md-4">
<!-- Sidebar - takes the remaining 4 columns -->
</div>
</div>
<!-- No manual percentage math, no custom CSS -
just column counts that add up to 12 -->
Cards: Wrapping Each Task Properly
A card is a bordered, padded container with a consistent visual style, genuinely useful for turning a bare list into something that actually reads as a real UI element, not raw text.
<div class="card mb-2">
<div class="card-body d-flex justify-content-between align-items-center">
<span>Write documentation</span>
<button class="btn btn-sm btn-outline-danger">Delete</button>
</div>
</div>
<!-- mb-2 adds margin-bottom (spacing between cards)
d-flex, justify-content-between, and
align-items-center are Bootstrap's UTILITY classes -
small, single-purpose classes for common layout
needs, used here to put the task text and the
Delete button on opposite ends of the same row -->
Button Variants: Replacing Plain, Unstyled Buttons
<button class="btn btn-primary">Add Task</button>
<button class="btn btn-outline-danger">Delete</button>
<button class="btn btn-outline-secondary">All</button>
<!-- btn-primary - a solid, prominent color for the
main action (adding a task)
btn-outline-danger - a red outline, appropriate
for a destructive action (deleting), without being
as visually loud as a solid red button everywhere
btn-outline-secondary - a neutral, low-emphasis
style, appropriate for the filter buttons, which
aren't the primary focus of the page -->
A Navbar: Giving the App an Actual Header
<nav class="navbar navbar-dark bg-dark mb-4">
<div class="container">
<span class="navbar-brand mb-0 h1">Task Tracker</span>
</div>
</nav>
<!-- navbar-dark + bg-dark gives a dark background with
light text automatically color-adjusted for
contrast - no manual color picking required -->
The Complete Styled Task Tracker
@page "/tasks"
<nav class="navbar navbar-dark bg-dark mb-4">
<div class="container">
<span class="navbar-brand mb-0 h1">Task Tracker</span>
</div>
</nav>
<div class="container">
<div class="row mb-3">
<div class="col-md-9">
<input class="form-control" @bind="newTaskDescription" placeholder="New task..." />
</div>
<div class="col-md-3">
<button class="btn btn-primary w-100" @onclick="AddTask">Add Task</button>
</div>
</div>
<div class="mb-3">
@foreach (var task in FilteredTasks)
{
<div class="card mb-2">
<div class="card-body d-flex justify-content-between align-items-center">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="task.IsCompleted" />
@if (task.IsCompleted)
{
<s class="ms-2">@task.Description</s>
}
else
{
<span class="ms-2">@task.Description</span>
}
</div>
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteTask(task)">Delete</button>
</div>
</div>
}
</div>
<div class="btn-group">
<button class="btn btn-outline-secondary" @onclick='() => currentFilter = "All"'>All</button>
<button class="btn btn-outline-secondary" @onclick='() => currentFilter = "Active"'>Active</button>
<button class="btn btn-outline-secondary" @onclick='() => currentFilter = "Completed"'>Completed</button>
</div>
</div>
@code {
// IDENTICAL to Part 4 - zero logic changes.
// Every single change in this post is a class
// attribute added to existing markup
private List<TaskItem> tasks = new();
private string newTaskDescription = string.Empty;
private string currentFilter = "All";
private IEnumerable<TaskItem> FilteredTasks => currentFilter switch
{
"Active" => tasks.Where(t => !t.IsCompleted),
"Completed" => tasks.Where(t => t.IsCompleted),
_ => tasks
};
private void AddTask()
{
if (!string.IsNullOrWhiteSpace(newTaskDescription))
{
tasks.Add(new TaskItem { Description = newTaskDescription, IsCompleted = false });
newTaskDescription = string.Empty;
}
}
private void DeleteTask(TaskItem task) => tasks.Remove(task);
}
Worth noticing directly: the @code block is completely unchanged from Part 4. Every single addition in this post, navbar, card, btn-primary, row and col-md classes, is a CSS class attached to existing markup. This is the practical meaning of separating structure and logic, Blazor's job, from presentation, Bootstrap's job: styling an app doesn't require touching the code that makes it actually work.
The Series, Complete
This closes the WPF, MVVM, Razor, Blazor, Bootstrap series. Part 1 built a working Task Tracker in plain WPF, honestly naming the pain points left in on purpose. Part 2 refactored that exact app into proper MVVM, fixing every one of those pain points directly, tangled logic separated, the missed-refresh bug eliminated structurally, the filtering logic made genuinely unit-testable. Part 3 pivoted to the web, rendering the same task data with Razor, deliberately read-only. Part 4 made that page interactive with Blazor, using C# instead of JavaScript, arriving at genuine feature parity with the WPF and MVVM version, add, check, delete, filter, through a completely different technology stack. Part 5, this post, finished the job visually, applying Bootstrap on top of already-working Blazor logic with zero changes to that logic at all.
One example, five posts, two genuinely different technology stacks, WPF desktop and Blazor web, built to the same functional specification, which is itself a useful thing to be able to say plainly in an interview: the same problem, solved twice, in two different paradigms, understanding what changed and what stayed conceptually the same across both.
Key Lessons
Bootstrap is a pre-written CSS framework, classes for layout, components, and utilities, letting a page look genuinely designed without writing custom CSS from scratch.
The 12-column grid system, row and col-md classes, handles layout proportions without manual percentage math.
Cards, button variants, and the navbar are pre-styled components, dropping a class onto existing markup, not building visual style from raw CSS.
Applying Bootstrap required zero changes to the Blazor @code block, proof that structure and logic and visual presentation are genuinely separable concerns.
Across this five-part series, the same Task Tracker was built twice, in WPF and MVVM, and in Razor, Blazor, and Bootstrap, the same functional requirements, two different technology stacks, each with its own idioms for solving the same underlying problems, keeping UI in sync with data, separating concerns, styling presentation.
Summary
Bootstrap turns a functionally complete but visually plain Blazor app into something genuinely presentable, using a pre-written grid system, styled components, and utility classes, all applied without touching a single line of the underlying Blazor logic. That separation is the point: the same Task Tracker that worked correctly in Part 4 works identically here, just dressed properly. Across all five posts in this series, one example carried through two genuinely different technology stacks, a plain WPF window refactored into proper MVVM, and a Razor page made interactive with Blazor and then styled with Bootstrap, arriving at the same functional result from two different directions, which is exactly the kind of comparative understanding worth having ready for an interview question about either one.
More from TechStack Blog: C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.