Skip to main content
PProfileMatch

CSS Grid and Responsive Design With Media Queries

Web DevelopmentLesson 4 of 45 min read

What you'll learn

  • Define columns and rows with grid-template-columns, fr, repeat and minmax
  • Build a self-adjusting card grid with auto-fit and a page layout with grid areas
  • Write mobile-first media queries and choose breakpoints from content
  • Use fluid images and clamp() so type and media scale with the viewport

On this page

  1. Grid in two dimensions
  2. Naming regions with grid areas
  3. Responsive design: the working recipe
  4. Common mistakes
  5. How this shows up in real projects and interviews

A greenhouse manager wants a sensor dashboard: temperature, humidity, soil moisture, light and more, one card each. On a wall monitor she wants four cards across. On a tablet, three. On a phone, one per row. You could write three layouts and switch between them with a pile of rules, or you could describe the intent once and let the browser compute the number of columns. CSS Grid lets you do the second.

Grid in two dimensions

Setting display: grid on a container creates a grid of rows and columns, and its direct children are placed into the cells. Unlike flexbox, you define the tracks on the container (grid-template-columns, grid-template-rows) and the items fill them in order. The key new unit is fr, a fraction of the free space. grid-template-columns: 2fr 1fr makes two columns where the first is twice as wide as the second, after any fixed sizes and gaps are subtracted.

greenhouse.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Greenhouse dashboard</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; font-family: system-ui, sans-serif; background: #eef3ee; }
    .wrap { max-width: 1000px; margin: 0 auto; padding: 1rem; }

    .cards {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 20px;
    }
    .card { background: white; border: 1px solid #c9d8c9; border-radius: 8px; padding: 1rem; }
    .card h2 { margin: 0 0 0.5rem; font-size: 1rem; }
    .value { font-size: 2rem; font-weight: 700; }
  </style>
</head>
<body>
  <div class="wrap">
    <h1>Greenhouse 3</h1>
    <section class="cards">
      <article class="card"><h2>Temperature</h2><div class="value">24.5 C</div></article>
      <article class="card"><h2>Humidity</h2><div class="value">61 %</div></article>
      <article class="card"><h2>Soil moisture</h2><div class="value">38 %</div></article>
      <article class="card"><h2>Light</h2><div class="value">12.4 klx</div></article>
      <article class="card"><h2>CO2</h2><div class="value">540 ppm</div></article>
      <article class="card"><h2>Water tank</h2><div class="value">82 %</div></article>
    </section>
  </div>
</body>
</html>

On a wide monitor the wrapper is 1000 pixels wide with 16 pixels of padding, so the grid is 968 pixels. How many 220-pixel columns fit with 20-pixel gaps? Four columns need 4 x 220 + 3 x 20 = 940 pixels, which fits; five would need 1180, which does not. So the six cards form two rows: four cards in the first, two in the second, and each card stretches (the 1fr maximum) to (968 - 60) / 4 = 227 pixels. On a 768-pixel tablet the grid is 736 pixels wide, three columns fit (3 x 220 + 2 x 20 = 700, four would need 940), and on a 360-pixel phone only one does, so the cards stack. You wrote no media query at all: auto-fit plus minmax is an intrinsic, self-adjusting layout.

Naming regions with grid areas

For whole-page layouts, grid areas read almost like a drawing. You name regions on the container and assign each child to a name. This is also the natural place for media queries: the markup stays identical and only the drawing changes.

page-layout.css
/* Mobile first: a single column, source order is the reading order */
.page {
  display: grid;
  gap: 1rem;
  grid-template-areas:
    "head"
    "nav"
    "main"
    "foot";
}
.page > header { grid-area: head; }
.page > nav    { grid-area: nav;  }
.page > main   { grid-area: main; }
.page > footer { grid-area: foot; }

/* From 48rem (768px at the default font size) upwards: sidebar layout */
@media (min-width: 48rem) {
  .page {
    grid-template-columns: 14rem 1fr;
    grid-template-areas:
      "head head"
      "nav  main"
      "foot foot";
  }
}

Below 768 pixels the header, navigation, main content and footer stack in one column. At or above it, the header and footer span both columns, while the navigation sits in a 14 rem sidebar to the left of the main content, which takes the remaining width. The HTML did not change.

Responsive design: the working recipe

  1. Include <meta name="viewport" content="width=device-width, initial-scale=1"> in the head (see HTML document structure); without it phones render at a virtual desktop width and your media queries never trigger as expected.
  2. Write the small-screen styles first, with no media query, and use min-width queries to add layout as space becomes available. This is called mobile-first and keeps the default rules simple.
  3. Choose breakpoints where your content starts to look cramped or stretched, not at the pixel widths of a particular phone.
  4. Use flexible units (%, fr, rem, ch) and max-width instead of fixed pixel widths wherever you can.
  5. Make media fluid: img, video { max-width: 100%; height: auto; }.
Fluid sizing helpers
img, video { max-width: 100%; height: auto; }

h1 {
  /* min 1.75rem, grows with the viewport, max 3rem */
  font-size: clamp(1.75rem, 1rem + 3vw, 3rem);
}

.wrap {
  width: min(100% - 2rem, 62rem);   /* 1rem of space each side, never wider than 62rem */
  margin-inline: auto;
}

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

clamp(min, preferred, max) picks the preferred value but never goes below the minimum or above the maximum, so headings scale smoothly without a stack of breakpoints. Media queries are not only for width: prefers-reduced-motion, prefers-color-scheme and print let the page respond to the user's environment and settings.

NeedBetter choiceReason
A row of toolbar itemsFlexboxContent-driven, one dimension
Page frame with header, sidebar, footerGrid with areasRows and columns are both defined
Card gallery with aligned tracksGrid with auto-fitColumns stay aligned across rows
Centring one thingEitherBoth are one or two lines
Spacing between itemsgap on eitherNo margin hacks needed

Common mistakes

  • No viewport meta tag. On a phone the page appears zoomed out as though it were 980 pixels wide, and Lighthouse reports Does not have a <meta name="viewport"> tag with width or initial-scale.
  • Using `max-width` queries for everything. Desktop-first rules need to be undone on small screens, so they grow longer and fight themselves. Start small and add with min-width.
  • Fixed pixel widths on containers. A width: 1000px box forces horizontal scrolling on any narrower screen; use max-width.
  • Overlapping or gappy breakpoints. Mixing max-width: 768px and min-width: 768px makes 768 pixels match both. Pick one style and stick to it.
  • Items with long unbreakable content blowing out a grid track. Use minmax(0, 1fr) instead of plain 1fr when content such as a long URL or a table must be allowed to shrink.
  • Hiding content on mobile with `display: none` and calling the page responsive. People on phones need the same information; rearrange it instead.

How this shows up in real projects and interviews

Real design systems define a handful of breakpoints and a grid of columns, and component libraries are mostly flexbox and grid under the hood. In interviews, expect "what is mobile-first?", "flexbox versus grid" and "how would you build a responsive card grid without media queries?", for which the repeat(auto-fit, minmax(...)) pattern here is the standard answer. If you move on to a framework such as Next.js, the layout ideas carry over unchanged because the output is still HTML and CSS.

Try it yourself

Build a landing page for a fictional community bike-share with a header, a left navigation, a main area containing six station cards, and a footer. On a narrow screen everything stacks; from 48rem the nav sits at the left in a 12rem column and the cards form an auto-fit grid with a 200px minimum. Resize the window and note at which widths the card count changes. Use DevTools device mode to check a 360px phone.

Show a hint

Apply grid-template-areas to the page and a nested grid with repeat(auto-fit, minmax(200px, 1fr)) to the cards. Remember the viewport meta tag, or device mode will not behave like a real phone.

Key takeaways

  • Grid defines rows and columns on the container; fr shares leftover space and repeat(auto-fit, minmax(...)) yields a self-adjusting card grid.
  • Grid areas let you redraw a whole page layout with a media query while the HTML stays unchanged.
  • Go mobile-first: base styles for small screens, then min-width queries at the points where the content needs more room.
  • Use max-width: 100% on media, clamp() for fluid type and the viewport meta tag so queries behave on phones.

Test yourself

A trick from this lesson's ride: auto-FIT fits the items to the row; auto-FILL fills the row with empty slots. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What does fr mean in CSS Grid?

fr stands for fraction of the available space in the grid container. After fixed sizes and gaps are subtracted, the leftover width is split among fr tracks in proportion to their numbers, so 2fr 1fr gives the first column twice the width of the second.

What is mobile-first CSS?

It means writing your default styles for small screens and then adding layout changes inside min-width media queries as the viewport grows. It keeps the base CSS simple, avoids undoing desktop rules on phones, and usually results in less code than a desktop-first approach.

How do I make a responsive layout without media queries?

Use intrinsic sizing. A grid with repeat(auto-fit, minmax(220px, 1fr)) adds or removes columns as space changes, flexbox with flex-wrap and a flex-basis does the same for rows, and clamp() or min() handle fluid sizes. You will still want queries for larger structural changes.