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, md and lg versions take over from that width up.
  • row-cols-1 row-cols-sm-2 row-cols-lg-3 shows one card per row on phones, two from small screens up and three from large ones, with one col per item.
  • A pager's links carry the page in the query string, such as /products?page=2 from asp-route-page, and the current page's item gets active.
  • OFFSET and FETCH NEXT fetch one page of rows, and need an ORDER BY so 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.

Key terms

Cheat sheets