Mobile-First Layouts with Bootstrap
Build pages that work on a phone first, then widen. Bootstrap's row-cols classes, cards, a collapsing navbar and pagination turn a Razor loop into a responsive product grid.
Journeyman · ASP.NET Core · CSS · Updated
After this lesson you can
- Lay out a list of items as a responsive grid with Bootstrap's row-cols classes, starting from the phone
- Build a pager for a long list with Bootstrap's pagination and the page in the query string
Key takeaways
- Mobile-first means designing for the phone first: Bootstrap classes without a size apply to every screen, and
sm,mdandlgversions take over from that width up. row-cols-1 row-cols-sm-2 row-cols-lg-3shows one card per row on phones, two from small screens up and three from large ones, with onecolper item.- A pager's links carry the page in the query string, such as
/products?page=2fromasp-route-page, and the current page's item getsactive. OFFSETandFETCH NEXTfetch one page of rows, and need anORDER BYso every page sees the rows in the same order.- Check every page in the browser's phone view (F12, then the device button): the cards stack, the menu folds, and nothing scrolls sideways.
More visitors use a phone than a computer. Mobile-first means designing for the narrow screen first, then letting the page spread out as the screen grows. Bootstrap, from Bootstrap 5, is built this way: its classes without a size apply to every screen, and sm, md, lg versions take over from that width up.
A responsive grid of cards
The product list puts its cards in a grid that changes with the screen:
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-3">
@foreach (Product product in Model)
{
<div class="col">
<partial name="_ProductCard" model="product" />
</div>
}
</div>
| Class | Means |
|---|---|
row-cols-1 |
One card per row: phones, the starting point |
row-cols-sm-2 |
Two per row from small screens (576px) up |
row-cols-lg-3 |
Three per row from large screens (992px) up |
g-3 |
A gap between the cards |
col |
Each card's column, sized by the row's row-cols |
The Razor loop doesn't change at all: one col per product, and Bootstrap decides how many fit across. In each card, h-100 makes every card in a row as tall as the tallest.
The navbar folds away
The template's layout already uses a responsive navbar: on a wide screen the links sit in a row; on a phone they fold into a menu button.
<nav class="navbar navbar-expand-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-controller="Home" asp-action="Index">NorthwindWeb</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="Products" asp-action="Index">Products</a>
</li>
</ul>
<div class="navbar-nav">
<partial name="_LoginStatus" />
</div>
</div>
</div>
</nav>
navbar-expand-sm says when to stop folding: from small screens up, the links show in a row.
A pager for a long list
Seventy-seven products are too many for one phone screen. Show twelve at a time, with Bootstrap's pagination below, and the page number in the query string (from Route Values and the Query String):
@{
int page = (int)ViewData["Page"]!;
int pageCount = (int)ViewData["PageCount"]!;
}
<nav aria-label="Product pages" class="mt-4">
<ul class="pagination">
@for (int i = 1; i <= pageCount; i++)
{
<li class="page-item @(i == page ? "active" : "")">
<a class="page-link" asp-action="Index" asp-route-page="@i">@i</a>
</li>
}
</ul>
</nav>
The links come out as /products?page=1 to /products?page=7, and the current page's item gets the active class. On the server, the DAO fetches just one page with OFFSET and FETCH:
SELECT ProductId, ProductName, CategoryId, UnitPrice, UnitsInStock
FROM Products
ORDER BY ProductName
OFFSET @Skip ROWS FETCH NEXT @PageSize ROWS ONLY;
OFFSET skips the earlier pages' rows, and FETCH NEXT takes one page. It needs an ORDER BY, so every page sees the rows in the same order.
Check it on a phone
Every browser can pretend to be a phone: press F12 for the developer tools, then the device button (or Ctrl+Shift+M), and pick a phone. Check that the cards stack, the menu folds, and nothing makes the page scroll sideways.
Check yourself
2 questions to try on the lesson page.
So what
Start narrow, then add columns as the screen grows: a row-cols grid of partial-view cards, a navbar that folds, and a pager, and the same Razor works on every screen.