Skip to content

Accessible HTML and Maintainable CSS

Step 15 of 45

Flexbox, Grid, and Responsive Design

Introduction

This is the second lesson of chapter 4, aligned with course 4 of the Meta Front-End Developer pathway ("HTML and CSS in Depth"). In lesson 1 you built an accessible booking form. Now you make the page that surrounds it behave on every screen size. Most visitors to a workshop site arrive on a phone, some on a laptop with a narrow window, and a few on a large monitor. A layout that only works at one width fails most of them.

CSS gives you two modern layout systems. Flexbox arranges items along one axis (a row or a column). Grid arranges items on two axes at once (rows and columns). Choosing the right one, and combining them with fluid sizing and a small number of well-chosen breakpoints, is the core skill of this lesson. The practice scenario is original: the Harbor Workshops event listing, a set of workshop cards that must stay stable, never overflow horizontally, and keep text at a readable length.

Learning objectives

  1. Explain the difference between one-dimensional (Flexbox) and two-dimensional (Grid) layout and choose between them.
  2. Use display: flex, flex-direction, justify-content, align-items, gap, flex-wrap, and the flex shorthand.
  3. Build grids with grid-template-columns, repeat(), fr, minmax(), auto-fit, and grid-template-areas.
  4. Size elements fluidly with relative units, min(), max(), clamp(), and max-width.
  5. Write mobile-first media queries with content-driven breakpoints and explain the viewport meta tag.
  6. Prevent horizontal overflow with responsive images, min-width: 0, and overflow-wrap.
  7. Keep line lengths readable with ch units.
  8. Integrate Bootstrap's responsive grid and utilities without fighting your own CSS.

1. The box model and sizing, a short refresher

Every element is a box: content, padding, border, margin. By default width sets the content width, so a width: 300px box with padding: 20px and a 4px border is 348px wide. Most projects start with:

*, *::before, *::after {
  box-sizing: border-box;
}

With border-box, width includes padding and border, so width: 300px really occupies 300 pixels. Without it, percentage widths plus padding quickly produce overflow, a classic cause of the horizontal scrollbar.

Block elements fill the available width; inline elements flow within a line. Flexbox and Grid change how children of a container are laid out. You apply display: flex or display: grid to the parent (the container), and the direct children become items.

2. Flexbox: one-dimensional layout

The two axes

A flex container has a main axis (the direction items flow) and a cross axis (perpendicular). flex-direction: row (default) puts the main axis horizontal; column makes it vertical. The alignment properties are defined in terms of these axes:

Property Applies to Controls
justify-content Container Distribution along the main axis
align-items Container Alignment of items on the cross axis
align-content Container Distribution of wrapped lines on the cross axis
gap Container Space between items (no outer space)
flex-wrap Container Whether items wrap onto new lines
flex Item Grow, shrink, and base size
align-self Item Override of align-items for one item
order Item Visual order (does not change keyboard order)

A first example: a header

<header class="site-header">
  <a class="logo" href="/">Harbor Workshops</a>
  <nav>
    <ul class="nav-list">
      <li><a href="/workshops">Workshops</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>
</header>
.site-header {
  display: flex;
  justify-content: space-between; /* logo left, nav right */
  align-items: center;            /* vertical centering */
  gap: 1rem;
  padding: 1rem;
}
.nav-list {
  display: flex;
  gap: 1.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

The header's two children (logo and nav) sit on one row with space between. The ul is a second flex container, so its li items form a row. Flexbox is nested: any item can itself be a container.

The flex shorthand

flex: 1 1 200px means flex-grow: 1, flex-shrink: 1, flex-basis: 200px.

  • flex-basis is the starting size along the main axis.
  • flex-grow says how remaining space is shared. Two items with flex-grow: 1 share it equally; 2 and 1 share it two to one.
  • flex-shrink says how items shrink when space is short. 0 forbids shrinking.

Common values: flex: 1 (equal share, basis 0), flex: none (rigid at natural size), flex: auto (grow and shrink from natural size).

Wrapping and the minimum-size trap

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.tag-list li { flex: 0 0 auto; }

With flex-wrap: wrap tags move onto a new line instead of shrinking or overflowing. Without wrap, a flex row never wraps; items shrink until their minimum content size, and then overflow.

That leads to a famous bug. A flex item has an automatic minimum size equal to its content (min-width: auto). A long unbroken string such as an email or a URL refuses to shrink and pushes the layout wider than the screen. The fix:

.card-body {
  min-width: 0;            /* allow the item to shrink below its content */
  overflow-wrap: anywhere; /* allow long strings to break */
}

Centering

The classic "center anything" is now two lines:

.hero {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 60vh;
}

Use min-height, not height, so the box grows if the content is taller than expected (for example on a narrow phone or with a larger user font size).

3. Grid: two-dimensional layout

Defining tracks

.event-grid {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  gap: 1rem;
}

fr is a fraction of the free space. Three tracks of 1fr 2fr 1fr share the width one quarter, one half, one quarter, after gaps are subtracted. You can mix units: grid-template-columns: 240px 1fr gives a fixed sidebar and a flexible main column.

repeat(3, 1fr) is shorthand for 1fr 1fr 1fr. Rows are created automatically as needed (the implicit grid), and grid-auto-rows controls their height.

The responsive grid without media queries

The single most useful Grid recipe:

.event-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1.5rem;
}

Read it as: "make as many columns as fit, each at least 16rem wide, and share any leftover space equally." On a 360 px phone you get one column; at 700 px two columns; at 1100 px three or four. No media query was needed because the grid reacts to the available space.

Difference between auto-fit and auto-fill: with few items, auto-fill keeps empty tracks, so items stay narrow; auto-fit collapses empty tracks, so the existing items stretch. For a card list, auto-fit is usually what you want.

A warning: minmax(16rem, 1fr) can overflow on a screen narrower than 16rem plus padding (small phones in a narrow container). A defensive form is minmax(min(16rem, 100%), 1fr), which never asks for more than the container width.

Placing items and named areas

.page {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas:
    "header"
    "main"
    "sidebar"
    "footer";
  gap: 1rem;
}
@media (min-width: 48rem) {
  .page {
    grid-template-columns: 2fr 1fr;
    grid-template-areas:
      "header  header"
      "main    sidebar"
      "footer  footer";
  }
}
header  { grid-area: header; }
main    { grid-area: main; }
aside   { grid-area: sidebar; }
footer  { grid-area: footer; }

Named areas make the layout readable as a map. Note that you changed the visual arrangement only; the HTML source order (header, main, aside, footer) stays logical, which keeps keyboard and screen reader order sensible. Avoid using order or grid placement to reorder content in ways that contradict the reading order.

Items can also span tracks: grid-column: span 2 makes a featured card two columns wide.

Flexbox or Grid?

Question Flexbox Grid
Layout direction One axis at a time Rows and columns together
Driven by Content (items decide their size) Layout (tracks decide the size)
Typical use Nav bars, button rows, centering, card internals Page skeletons, card galleries, dashboards
Alignment across rows Each wrapped line is independent Columns align across all rows
Overlap Awkward Easy

A quick test: if you want items in different rows to line up in columns, use Grid. If you are laying out a line of things of unequal size, use Flexbox. In real pages you combine them: Grid for the page and gallery, Flexbox inside each card.

4. Fluid sizing

Relative units

Unit Relative to Good for
% Parent's size Widths inside a container
rem Root font size (usually 16 px) Spacing and font sizes that respect user settings
em The element's own font size Padding that scales with a component's text
vw / vh 1% of viewport width / height Large hero sections
ch Width of the "0" character Line length
fr Free space in a grid Grid tracks

Prefer rem for fonts and spacing so that users who raise their browser font size get a proportionally larger layout. Fixed px heights for text containers cause clipped text when the font grows.

min(), max(), and clamp()

.container {
  width: min(100% - 2rem, 70rem);  /* never wider than 70rem, always 1rem margins */
  margin-inline: auto;
}
h1 {
  font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
}

clamp(min, preferred, max) returns the preferred value limited to a range. The heading scales smoothly between 1.75rem and 3rem. Include a rem part in the preferred value so the size still responds to user font settings.

Readable line length

Long lines are hard to read. Limit prose to roughly 45 to 75 characters:

.prose { max-width: 65ch; }

5. Responsive design and breakpoints

The viewport meta tag

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

Without it, mobile browsers pretend the screen is about 980 px wide and shrink the page, so your media queries never trigger as expected. Never disable user zoom (user-scalable=no, maximum-scale=1), which harms accessibility.

Mobile-first media queries

Write the base styles for the smallest screens, then add complexity with min-width queries:

.event-card { padding: 1rem; }

@media (min-width: 40rem) {
  .event-card { padding: 1.5rem; }
}
@media (min-width: 64rem) {
  .event-grid { gap: 2rem; }
}

Using rem in media queries makes breakpoints respect user font size too. Mobile-first usually yields less code, because phones get the simple single-column layout without overrides to undo.

Content-driven breakpoints

Do not choose breakpoints from a list of device names, because devices change every year. Instead, open the page, slowly widen the window, and add a breakpoint where the design starts to break: where lines get too long, where cards look cramped, or where a nav no longer fits. The numbers are whatever your content needs. Keep the count small; three or fewer is common when Grid and clamp() do much of the work.

Container queries, an alternative

Media queries look at the viewport. Container queries let a component respond to the size of its container, which suits reusable cards placed in sidebars and wide columns:

.card-slot { container-type: inline-size; }

@container (min-width: 28rem) {
  .event-card { display: grid; grid-template-columns: 8rem 1fr; gap: 1rem; }
}

Container queries are supported by current major browsers (see MDN for the up-to-date compatibility table). The certificate coursework focuses on media queries, so master those first.

Other useful media features

@media (prefers-color-scheme: dark) { /* dark theme */ }
@media (prefers-reduced-motion: reduce) { /* calmer animation */ }
@media (hover: hover) { /* hover styles only for devices that can hover */ }
@media print { /* print stylesheet */ }

6. Images, overflow, and stable cards

Responsive images

img {
  max-width: 100%;
  height: auto;
  display: block;
}

max-width: 100% stops an image from exceeding its container, and height: auto preserves its proportions. Always include width and height attributes in the HTML (or aspect-ratio in CSS) so the browser reserves space before the image loads and the page does not jump (layout shift).

<img src="pottery-640.jpg"
     srcset="pottery-640.jpg 640w, pottery-1280.jpg 1280w"
     sizes="(min-width: 64rem) 25vw, 100vw"
     width="640" height="400"
     alt="Hands shaping a clay bowl on a pottery wheel"
     loading="lazy">

srcset and sizes let the browser download a file sized for the layout. loading="lazy" defers off-screen images, but do not lazy-load the main hero image. The alt text describes the image's purpose; use alt="" for purely decorative images.

For uniform thumbnails use:

.card img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

Finding the cause of horizontal overflow

A horizontal scrollbar on a phone means something is wider than the viewport. Common culprits:

  1. A fixed width in px larger than the screen.
  2. width: 100% plus padding without border-box.
  3. Images or tables without max-width: 100% or a scroll wrapper.
  4. A flex or grid item with a long word and no min-width: 0 or overflow-wrap.
  5. 100vw used for width: it includes the vertical scrollbar width on some desktops and causes a few pixels of overflow.
  6. Negative margins or absolutely positioned elements.

Do not hide the problem with overflow-x: hidden on body; that can cut off content and break sticky positioning. Find and fix the element (see lesson 4).

7. Responsive Bootstrap integration

Bootstrap (introduced in chapter 1) provides a 12-column grid and utility classes. Its breakpoints are sm 576, md 768, lg 992, xl 1200 and xxl 1400 pixels, applied mobile-first with min-width.

<div class="container">
  <div class="row g-4">
    <div class="col-12 col-md-6 col-lg-4">
      <article class="card h-100">
        <img src="pottery-640.jpg" class="card-img-top" alt="" width="640" height="400">
        <div class="card-body">
          <h3 class="card-title h5">Pottery for Beginners</h3>
          <p class="card-text">Saturday, 7 November</p>
        </div>
      </article>
    </div>
    <!-- more columns -->
  </div>
</div>

col-12 col-md-6 col-lg-4 reads: full width by default, half width from 768 px, one third from 992 px. g-4 adds gutters, h-100 makes cards in a row equal height. Bootstrap's grid is Flexbox-based internally, so the same concepts apply.

Good practice when mixing Bootstrap and your own CSS:

  • Prefer Bootstrap's classes and Sass/CSS variables for what it provides; write custom CSS for what is unique.
  • Do not override Bootstrap with !important; use a more specific selector in a later stylesheet (see lesson 3 on specificity).
  • Load your stylesheet after Bootstrap's so equal-specificity rules win.
  • Keep the viewport meta tag; Bootstrap depends on it.

Worked example: Harbor Workshops event listing

Brief. Build an event listing. Each card shows an image, a title, a date, a long description and a "Book" button. Requirements: one column on a phone, more columns as space allows, equal-height cards with the button at the bottom, no horizontal overflow at 320 px, and readable text.

Step 1: structure. Use semantic markup: a section with a heading, a ul of cards (a list of items), each an article.

<section aria-labelledby="events-title">
  <h2 id="events-title">Upcoming workshops</h2>
  <ul class="event-grid">
    <li class="event-card">
      <img src="pottery-640.jpg" width="640" height="400" alt="">
      <div class="event-body">
        <h3>Pottery for Beginners</h3>
        <p><time datetime="2026-11-07">Saturday, 7 November</time></p>
        <p>Learn to centre clay and shape a bowl. Bring an apron; all tools, glaze and firing are included in the price of the session.</p>
        <a class="button" href="/book/pottery">Book seats</a>
      </div>
    </li>
    <!-- more cards -->
  </ul>
</section>

Step 2: the grid.

.event-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: 1.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

Step 3: the card as a flex column. The card is a flex container in the column direction so that the body can stretch and the button sits at the bottom.

.event-card {
  display: flex;
  flex-direction: column;
  border: 1px solid #ccd3da;
  border-radius: 0.5rem;
  overflow: hidden;
}
.event-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
.event-body {
  display: flex;
  flex-direction: column;
  flex: 1;               /* fill the remaining card height */
  gap: 0.75rem;
  padding: 1rem;
  min-width: 0;
  overflow-wrap: anywhere;
}
.event-body .button { margin-top: auto; align-self: flex-start; }

margin-top: auto in a flex column absorbs all free space above the button and pushes it to the bottom, a handy pattern.

Step 4: the container and text.

.container { width: min(100% - 2rem, 72rem); margin-inline: auto; }
.event-body p { max-width: 60ch; }

Step 5: check. At 320 px: one column of width about 288 px. At 700 px: two columns. At 1200 px: three or four columns. Cards in a row have equal height because grid rows stretch items by default.

Why not the alternatives? A Flexbox row with flex-wrap also wraps cards, but the last row's cards stretch to different widths and columns do not align. Fixed-width cards with float break on small screens. A separate media query for every device would encode today's phones into the code. The Grid recipe adapts to any width with no breakpoint.

Common pitfalls

  • Forgetting box-sizing: border-box, then wondering why width: 100% plus padding overflows.
  • justify-content vs align-items confusion. In a column flex container, they swap axes.
  • Using height in px for text boxes, clipping content when the font grows.
  • Choosing breakpoints by device name instead of by content.
  • Desktop-first CSS with many max-width overrides, which is longer and harder to maintain.
  • overflow-x: hidden as a cure that hides rather than fixes overflow.
  • Reordering with order or grid placement so visual order differs from the DOM order, confusing keyboard users.
  • Missing viewport meta tag, so the mobile layout never applies.
  • Images without dimensions, causing layout shift.

Exam tip: When a question describes items that must line up in rows and columns, think Grid. When it describes distributing items along a single line or centering, think Flexbox. When it describes a layout that "adapts without media queries," think auto-fit with minmax() or clamp(). When it describes content extending past the right edge on a phone, think fixed widths, missing min-width: 0, or images without max-width: 100%.

Mini case study: the clipped booking page

Situation. Nadia reports that on her phone the Harbor Workshops page can be scrolled sideways, and the "Book seats" button is cut off. Her friend's tablet shows a layout identical to the desktop, only tiny. The CSS contains:

.page { width: 1100px; margin: 0 auto; }
.layout { display: flex; }
.main { width: 70%; padding: 2rem; }
.side { width: 30%; padding: 2rem; }

The HTML <head> has a charset meta tag but no viewport tag.

Questions. (1) Why does the tablet show a tiny desktop layout? (2) Why does the layout overflow on a phone? (3) How would you fix it?

Solution.

  1. Without <meta name="viewport" content="width=device-width, initial-scale=1">, mobile browsers render at an assumed width of about 980 px and scale down. Add the tag.
  2. .page has a fixed 1100px width, wider than any phone. Also, .main and .side use percentage widths with padding but no box-sizing: border-box, so 70% + 30% plus 8 rem of padding exceeds 100%.
  3. A fix, mobile-first:
*, *::before, *::after { box-sizing: border-box; }
.page { width: min(100% - 2rem, 70rem); margin-inline: auto; }
.layout { display: flex; flex-direction: column; gap: 1.5rem; }
.main, .side { padding: 1rem; }
@media (min-width: 48rem) {
  .layout { flex-direction: row; }
  .main { flex: 2; }
  .side { flex: 1; }
}

The phone gets a single column; at 48 rem (768 px at the default font size) the side panel appears next to the main content. No horizontal scroll remains because no width exceeds the container and padding is included in sizes. Verify by resizing to 320 px and confirming that the document width equals the viewport width.

Key terms

  • Flex container / item: the parent with display: flex and its direct children.
  • Main axis / cross axis: the direction of flow and its perpendicular.
  • fr unit: a fraction of free space in a grid.
  • Track: a grid row or column.
  • Implicit grid: rows or columns created automatically for items beyond the declared tracks.
  • Breakpoint: a viewport (or container) width at which the layout changes.
  • Mobile-first: base styles for small screens, enhanced with min-width queries.
  • Viewport meta tag: the <meta> tag that makes the layout width equal the device width.
  • clamp(): a CSS function returning a value limited to a minimum and maximum.
  • Layout shift: unexpected movement of content as the page loads.

Summary checklist

  • I apply box-sizing: border-box globally.
  • I choose Flexbox for one-dimensional flows and Grid for two-dimensional layouts.
  • I can explain justify-content, align-items, gap, flex-wrap, and flex: 1 1 auto.
  • I can write a responsive card grid with repeat(auto-fit, minmax(...)).
  • I use min-width: 0 and overflow-wrap to stop long content from forcing overflow.
  • I include the viewport meta tag and never disable zoom.
  • I write mobile-first media queries and place breakpoints where the content breaks.
  • I make images responsive with max-width: 100%, height: auto, dimensions and alt.
  • I limit prose width with ch and size text with rem and clamp().
  • I can read Bootstrap classes such as col-12 col-md-6 col-lg-4.
  • I test at 320 px and confirm there is no horizontal scrolling.

Official sources