Skip to content

Web Foundations: HTTP, Semantic HTML, and Responsive CSS

Step 3 of 45

Responsive Web Design with CSS Grid and Bootstrap 5.3

Introduction

Responsive web design with CSS Grid and Bootstrap 5.3 helps one interface work across narrow and wide screens. This lesson closes the first chapter and matches the responsive-layout and Bootstrap topics of course 1 of the Meta Front-End Developer pathway. Visitors reach Harbor Workshops on phones at 360px, tablets at 768px, laptops, and wide monitors. One fixed layout cannot serve them all. Responsive design means a single codebase that adapts its layout to the space and capabilities of the device.

You will learn the viewport, the mobile-first approach, media queries, fluid layout tools, reusable UI patterns, and the Bootstrap 5.3 grid and utility classes. You will also compare writing your own CSS with adopting a framework, and make that choice for a real scenario. The practice scenario is original: adapt the Harbor course catalog to phone and desktop widths.

Learning objectives

  1. Explain the role of the viewport meta tag and why it is required.
  2. Apply mobile-first thinking with min-width media queries.
  3. Build a fluid layout with relative units, max-width, and flexible images.
  4. Use the Bootstrap 5.3 container, row, column classes, breakpoints, and a few utility classes.
  5. Identify reusable UI patterns (card, navbar, form group).
  6. Weigh custom CSS against a framework for a given project.

1. The viewport

Mobile browsers have a concept of a layout viewport. When a page does not say otherwise, many phones assume a desktop-sized layout width (historically about 980px) and shrink the result to fit, making text tiny. Width queries then compare against that wide layout viewport: phone-specific max-width rules may not match, while larger-screen min-width rules may match unexpectedly.

The fix is one line in <head>:

HTML
<meta name="viewport" content="width=device-width, initial-scale=1">

width=device-width makes the layout viewport equal the device's width in CSS pixels. initial-scale=1 sets the initial zoom to 100%. Do not add user-scalable=no or maximum-scale=1: they prevent pinch zoom, which low-vision users depend on, and are an accessibility failure.

A CSS pixel is not a physical pixel. A phone with a 1170-physical-pixel-wide screen may report a viewport of 390 CSS pixels (a device pixel ratio of 3). All breakpoints use CSS pixels.

2. Mobile first

Mobile first means writing the base CSS for the smallest screens, then adding rules for larger ones using min-width queries. Why this order?

  • Small screens have the tightest constraints, so you decide priorities first: what content really matters.
  • The base styles are simple (a single column), and larger screens layer on enhancements.
  • Starting with narrow-screen constraints makes omissions easier to spot. This ordering alone does not reduce downloads: both approaches fetch the entire stylesheet when they use the same file.

Media queries

CSS
.catalog {
  display: grid;
  gap: 1rem;
  grid-template-columns: minmax(0, 1fr);         /* phone: one column */
}

@media (min-width: 40rem) {           /* about 640px */
  .catalog { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 64rem) {           /* about 1024px */
  .catalog { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

Read it as: one column by default; two columns when the viewport is at least 40rem; three at 64rem. Because queries use min-width, the 64rem rule also applies above it, and the later rule wins in a tie due to source order. Keep queries in increasing order.

Choose breakpoints where the content breaks, not at specific device models. Resize your browser, and when a line of text becomes too long or cards get cramped, that is a breakpoint. In media queries, rem and em use the initial font size influenced by user preferences, not an author-defined html font size. The approximate pixel comments assume an initial 16px font.

Other useful media features: prefers-reduced-motion, prefers-color-scheme, and orientation.

CSS
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

3. Fluid building blocks

A responsive page rests on a few habits, before any framework.

Relative sizing

CSS
.container {
  width: min(100% - 2rem, 70rem);
  margin-inline: auto;
}
img {
  max-width: 100%;
  height: auto;
}

min(100% - 2rem, 70rem) takes the smaller value: on phones, the full width minus a 1rem margin each side; on big screens, a cap of 70rem. margin-inline: auto centers the box. max-width: 100% stops images from overflowing their container, and height: auto preserves proportions.

Flexible text

CSS
h1 { font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem); }

clamp(min, preferred, max) scales the heading smoothly but never below 1.75rem or above 3rem.

Line length

Long lines hurt readability. Limit paragraphs to roughly 60 to 75 characters: max-width: 65ch; where ch is the width of the character "0".

Responsive images

HTML
<img
  src="/img/kiln-640.jpg"
  srcset="/img/kiln-640.jpg 640w, /img/kiln-1280.jpg 1280w"
  sizes="(min-width: 64rem) 33vw, 100vw"
  alt="A kiln glowing during a firing"
  width="640" height="427">

srcset offers candidates with their intrinsic widths; sizes tells the browser how wide the image will be displayed so it picks an appropriate file. The browser combines the declared display size with pixel density and other selection factors. A high-density phone can legitimately choose the 1280px candidate. Match sizes to the actual layout, including its gaps and width caps; this simplified example assumes roughly a full viewport below 64rem and a third above it.

Extend this example with Flexbox, Grid, and responsive breakpoints.

Layout modules in a nutshell

Flexbox is one-dimensional (a row or a column), good for navigation bars and aligning items. CSS Grid is two-dimensional (rows and columns together), good for page and catalog layouts. Lesson 4.2 covers both in depth; here we use them lightly.

CSS
.nav-list { display: flex; flex-wrap: wrap; gap: .5rem 1rem; }

flex-wrap: wrap lets items move to a new line instead of overflowing on narrow screens.

Worked example 1: a responsive catalog

Problem. Harbor has six workshop cards. Requirements: one column under 40rem, two columns from 40rem, three from 64rem. Cards must have equal gaps and never overflow at 320px.

Step 1: markup. Use a list inside a landmark.

HTML
<main>
  <h1>Workshops</h1>
  <ul class="catalog">
    <li class="card">
      <h2>Wheel basics</h2>
      <p>Mondays, 18:00. 6 seats.</p>
      <a href="/workshops/wheel-basics">Wheel basics details</a>
    </li>
    <!-- five more cards -->
  </ul>
</main>

Step 2: base (mobile) styles.

CSS
.catalog { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.card { min-width: 0; overflow-wrap: anywhere; border: 1px solid #c8c8c8; border-radius: .5rem; padding: 1rem; }

The explicit single column and min-width: 0 allow cards to shrink. overflow-wrap: anywhere prevents a long URL or identifier from forcing the layout wider.

Step 3: enhancements.

CSS
@media (min-width: 40rem) { .catalog { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .catalog { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

Result. At 320px: six stacked cards. At 800px: three rows of two. At 1200px: two rows of three. The zero track minimum and wrapping rules allow these text cards to shrink without horizontal scrolling. A plain 1fr track has an automatic minimum and can overflow with long unbreakable content.

Why not the alternatives? Fixed width: 300px cards with float: left need manual clearing and break at odd widths. A desktop-first design with max-width queries works, and downloads the same stylesheet if both approaches use one file. It requires careful narrow-screen overrides; mobile first makes those constraints the starting point. An auto-fit grid, repeat(auto-fit, minmax(16rem, 1fr)), is also a valid, query-free option; its trade-off is less explicit control over the exact column counts.

Extend this example with semantic HTML and accessible link text.

4. Reusable UI patterns

Interfaces repeat a small set of patterns. Recognizing them lets you build once and reuse.

Pattern Purpose Notes
Card Group an item's summary and action Used for workshops, tutors
Navigation bar Primary links, often collapsing on mobile Use <nav>
Form group Label plus control plus help or error text Always associate label with input
Alert / message Feedback after an action Use text, not color alone
Modal / dialog Focused task above the page Needs focus management; prefer <dialog>
Pagination / list Browse many items

A pattern is more than looks. A card that is entirely clickable needs one primary link, with accessible text; a collapsing menu needs a real <button> that reports whether it is expanded (aria-expanded).

5. Bootstrap 5.3

Bootstrap is a popular CSS framework (with optional JavaScript components) providing a grid, ready-made components, and utility classes. Version 5.3 does not require jQuery.

Adding Bootstrap

For a quick page you can use the CDN links from the official docs. In an npm project you install it:

Bash
npm install bootstrap@5.3

and import its stylesheet in your entry file, import "bootstrap/dist/css/bootstrap.min.css";. Always check the getting-started page of the Bootstrap version you use, and remember the viewport meta tag is a requirement of Bootstrap, too.

Breakpoints

Bootstrap 5.3 defines six default tiers, and its grid is mobile-first with min-width queries.

Breakpoint Class infix Viewport width
Extra small (none) under 576px
Small sm 576px and up
Medium md 768px and up
Large lg 992px and up
Extra large xl 1200px and up
Extra extra large xxl 1400px and up

Because the queries are min-width, a class like col-md-6 applies at md and every larger breakpoint.

Containers, rows, columns

The grid has a 12-column system. A container centers and constrains content, a row is a flex wrapper for columns, and columns take a number of the 12 slots.

HTML
<div class="container">
  <div class="row">
    <div class="col-12 col-md-6 col-lg-4">Card A</div>
    <div class="col-12 col-md-6 col-lg-4">Card B</div>
    <div class="col-12 col-md-6 col-lg-4">Card C</div>
  </div>
</div>

Reading col-12 col-md-6 col-lg-4: full width on phones (12 of 12), half (6 of 12) from 768px, a third (4 of 12) from 992px. Widths per column: 12/12 = 100%, 6/12 = 50%, 4/12 = 33.33%. If the sum on a row exceeds 12, the extra column wraps to the next line.

Shortcuts: row-cols-1 row-cols-md-2 row-cols-lg-3 on the row sets the number of columns per row without per-column classes. Gutters (spacing) use g-3 and similar classes, and .container-fluid spans the full width.

Utilities

Utility classes apply single rules directly.

Class Effect
mt-3, mb-4, p-2, px-3 Margin or padding on a spacing scale (0 to 5), per side or axis
d-none d-md-block Hidden below md, shown from md
d-flex justify-content-between align-items-center Flex row, space between, vertically centered
text-center, fw-bold, text-body-secondary Text alignment, weight, color
w-100 Width 100%

A Bootstrap version of the catalog

HTML
<main class="container py-4">
  <h1 class="mb-4">Workshops</h1>
  <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
    <div class="col">
      <article class="card h-100">
        <div class="card-body">
          <h2 class="h5 card-title">Wheel basics</h2>
          <p class="card-text">Mondays, 18:00. 6 seats.</p>
          <a class="btn btn-primary" href="/workshops/wheel-basics">Wheel basics details</a>
        </div>
      </article>
    </div>
    <!-- more columns -->
  </div>
</main>

h-100 makes cards in a row equally tall; h5 applies heading style to an <h2>, keeping the semantic level correct. This matches the earlier lesson: separate meaning from appearance.

Predicted output. One card per line under 768px, two per line from 768px, and three per line from 992px, with consistent gutters.

Framework or custom CSS?

Criterion Bootstrap Custom CSS
Speed to a working prototype Very fast Slower
Consistency across a team Built in, shared vocabulary Needs a style guide
Distinct brand identity Requires theming (Sass variables, CSS variables) Full control
File size Larger unless you import only what you use Only what you write
Learning curve Learn class names Learn CSS deeply
Accessibility Requires correct semantics, keyboard behavior, and contrast checks; defaults are not a compliance guarantee Also requires explicit implementation and review
Risk Sites can look alike; overriding fights specificity More code to maintain

In Bootstrap 5.3, text-muted is deprecated in favor of text-body-secondary. Interactive components such as collapsible navigation need Bootstrap's JavaScript or equivalent accessible behavior; importing CSS alone does not implement them.

A framework does not replace understanding CSS. When a Bootstrap layout misbehaves, you debug with the same box model, cascade, and Elements panel. Choose by project: an internal admin tool benefits from Bootstrap, and a brand-heavy marketing page may justify custom CSS. Many teams combine them: the grid and form styles from the framework, a thin layer of custom brand styles on top.

Note also that React projects frequently use component libraries, such as React-Bootstrap, which wrap Bootstrap's styles as React components. This pathway later returns to the question inside React.

6. Pitfalls and study tips

  • Forgetting the viewport meta tag, then wondering why media queries "do not work" on phones.
  • Mixing min-width and max-width queries in a confusing order.
  • Setting fixed widths in pixels on containers; use max-width.
  • Hiding content with d-none that screen-reader users still need; remember display: none also hides content from assistive technology.
  • Putting content directly in .row without .col children. A .row directly inside a .container is the normal pattern; its immediate content should be columns.
  • Testing only in one desktop window. Use the device toolbar in developer tools, then test a real phone.
  • Horizontal scrolling caused by one wide element; find it by temporarily outlining all elements (* { outline: 1px solid red; }).

Study tip. In Bootstrap, class prefixes apply "this breakpoint and up". Read column classes from smallest to largest.

Study tip. If phone-specific width queries fail to match, inspect the viewport meta tag and the actual layout viewport width before changing breakpoints.

Study tip. The mobile-first rule: base styles are for small screens; add min-width queries as the screen grows.

Mini case study: the squeezed booking page

Situation. Harbor's booking page uses this HTML:

HTML
<div class="container">
  <div class="row">
    <div class="col-8">Workshop details</div>
    <div class="col-4">Booking summary</div>
  </div>
</div>

On phones the two columns are so narrow that the summary text wraps every two words. A tester also reports that at 300% zoom on a laptop, the page still shows two cramped columns.

Tasks.

  1. Explain the cause.
  2. Propose a responsive fix.
  3. Describe how you would verify it.

Solution.

  1. col-8 and col-4 have no breakpoint infix, so they apply at all widths, from the smallest. At 360px the summary column is 4/12 of about 330px, around 110px. Browser zoom at 300% shrinks the effective viewport to roughly a third (for example, 1280 CSS pixels become about 427), which triggers the same narrow situation.
  2. Make the layout stack by default and split only from a larger tier: col-12 col-lg-8 and col-12 col-lg-4. On a phone each takes the full row; from 992px they sit side by side. Because zoom reduces the effective viewport, zoomed users also receive the stacked layout, which is what WCAG reflow requirements expect.
  3. Verification: use the device toolbar to check 360px, 768px, and 1200px widths; at a desktop viewport initially 1280 CSS pixels wide, zoom to 400% to approximate the WCAG reflow reference width of 320 CSS pixels, checking that this catalog needs no horizontal scrolling or overlapping text; check Elements to see that col-lg-8 computes to about 66.67% only at 992px and above.

Why not use d-none on the summary for mobile? The summary contains the price and cancellation terms, which phone users need. Hiding it removes important content instead of reflowing it.

Key terms

  • Responsive design: one page adapting to many screen sizes.
  • Layout viewport: the area used for page layout and width media queries. The visual viewport is the portion currently visible, which can differ during pinch zoom.
  • Mobile first: base CSS for small screens, min-width queries for larger ones.
  • Media query: a conditional block of CSS.
  • Breakpoint: the width at which the layout changes.
  • Fluid layout: uses relative sizes so content adapts.
  • Grid (Bootstrap): 12-column system of container, row, and column.
  • Utility class: a class applying one style rule.
  • Gutter: space between columns.

Summary checklist

  • My pages include width=device-width, initial-scale=1 and allow zoom.
  • Base styles target small screens; larger screens use min-width queries.
  • Images use max-width: 100% and have dimensions.
  • I can read a Bootstrap class such as col-12 col-md-6 col-lg-4 and state its width at each tier.
  • I know Bootstrap's six tiers and that they are min-width based.
  • I can compare a framework with custom CSS for a given project.
  • I tested at phone, tablet, and desktop widths and at high zoom.

Official sources