Intermediate45-60 minCSSLayoutModern CSS

Modern CSS Layout Extensions

Extend Flexbox and Grid with intrinsic grids, fluid CSS, :has(), subgrid, and layout stress testing.

Learning Objectives

By the end of this lesson, you'll be able to:

  • Choose between Flexbox, Grid, subgrid, and intrinsic sizing patterns
  • Build responsive card grids with auto-fit and minmax()
  • Use clamp(), min(), and max() for fluid spacing and sizing
  • Use :has() to adjust layout when optional content is present or missing
  • Explain Explain when subgrid helps nested alignment
  • Stress-test a layout with awkward real-world content

Why This Matters:

Modern CSS layout is not about memorising every new feature. It is about choosing the right tool for the layout problem in front of you.

Before You Start:

You should be familiar with:

Before You Write CSS, Ask Layout Questions

Modern CSS is not just about knowing more properties. It is about making better layout decisions before the stylesheet gets busy.

  1. Is this layout one-dimensional or two-dimensional?
  2. Do items need to line up across rows and columns?
  3. Does the component need to change when content is missing?
  4. Should the layout respond to the viewport or to its own available space?
  5. What happens when the content gets awkward?

That last question is the one that saves future pain. CSS behaves much better when we stop pretending all content will be polite.

Flow diagram showing layout questions that lead to Flexbox, Grid with minmax, :has(), subgrid, and stress testing.
Pick the tool after you identify the layout pressure. Modern CSS is most useful when it answers a specific content problem.

Starter HTML

You will build a reusable project card grid. The same pattern can become a tutorial card grid later, which makes it a good bridge into Container Queries and Modern CSS Architecture.

<section class="project-section">
  <div class="section-header">
    <p class="eyebrow">Featured projects</p>
    <h2>Build layouts that survive real content</h2>
    <p>
      These project cards will help us explore modern CSS layout patterns.
    </p>
  </div>

  <div class="project-grid">
    <article class="project-card project-card--feature">
      <img src="/images/project-cafe.jpg" alt="Cafe website preview" />
      <div class="project-card__content">
        <p class="project-card__type">Guided project</p>
        <h3>Black Swan Bistro</h3>
        <p>
          Build a restaurant website from layout plan to reusable components.
        </p>
        <a href="#">View project</a>
      </div>
    </article>

    <article class="project-card">
      <div class="project-card__content">
        <p class="project-card__type">Practice project</p>
        <h3>Accessibility Essentials</h3>
        <p>
          Improve structure, focus states, colour contrast, landmarks, and image text.
        </p>
        <a href="#">View project</a>
      </div>
    </article>

    <article class="project-card">
      <img src="/images/project-layout.jpg" alt="Website layout sketch" />
      <div class="project-card__content">
        <p class="project-card__type">CSS layout</p>
        <h3>Breaking Layouts Into Sections Without Losing Your Mind</h3>
        <p>
          Learn how to see a page as reusable regions instead of one giant blob.
        </p>
        <a href="#">View project</a>
      </div>
    </article>
  </div>
</section>

Base CSS

Start with a plain foundation. At this point, the page has content, readable defaults, and basic spacing. The layout extensions will come next.

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

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  color: #222;
  background: #f7f4ef;
}

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

a {
  color: currentColor;
  font-weight: 700;
}

.project-section {
  width: min(100% - 2rem, 70rem);
  margin-inline: auto;
  padding-block: 4rem;
}

.section-header {
  max-width: 42rem;
  margin-block-end: 2rem;
}

.eyebrow,
.project-card__type {
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

Use Intrinsic Grid Columns

A common beginner responsive grid uses one layout by default, then jumps to three columns at a specific breakpoint:

.project-grid {
  display: grid;
  gap: 1.5rem;
}

@media (min-width: 45rem) {
  .project-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

That works, but it creates a hard layout jump. A more resilient option lets the grid decide how many useful columns can fit:

.project-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
}

Read it in pieces. repeat(auto-fit, ...) tells the browser to fit as many columns as it can. minmax(min(18rem, 100%), 1fr) says each column should ideally be at least 18rem, never wider than the available space on tiny screens, and able to grow up to 1fr.

When the container becomes too narrow for three cards, the browser does not panic. It simply creates fewer columns. Small mercy. Big improvement.

Comparison of a hard breakpoint grid and an intrinsic grid that wraps cards as they reach their minimum readable width.
A hard breakpoint changes at one chosen width. An intrinsic grid responds when the cards themselves run out of useful space.

CSS Checkpoint for Understanding

Pause here and check whether the modern layout tools are solving real content problems rather than just adding fancy syntax.

  1. What happens when the project grid becomes too narrow for three readable cards?
  2. Why might :has(img) be better than adding a manual has-image class?
  3. When should you reach for subgrid?
Show sample answers
  1. The browser creates fewer columns. The minmax() minimum protects each card from becoming too cramped, and auto-fit lets the grid use the available space.
  2. The layout responds to the actual HTML. If the image is removed later, the card automatically stops using the image-specific layout.
  3. Use subgrid when nested content needs to align to tracks from the parent grid. If you only need a button at the bottom of a card, a simpler internal grid is usually enough.

How confident are you with this concept?

😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!

Use Fluid Spacing with clamp(), min(), and max()

Fixed spacing can feel too tight on large screens or too roomy on small screens. clamp() lets a value scale between a minimum and a maximum:

.project-section {
  width: min(100% - 2rem, 70rem);
  margin-inline: auto;
  padding-block: clamp(2.5rem, 8vw, 6rem);
}

.project-card {
  border: 1px solid #ddd3c7;
  border-radius: 1rem;
  overflow: hidden;
  background: #fff;
}

.project-card__content {
  display: grid;
  gap: clamp(0.75rem, 2vw, 1rem);
  padding: clamp(1rem, 3vw, 1.5rem);
}

Read clamp(2.5rem, 8vw, 6rem) like this: never smaller than 2.5rem, prefer a value that scales with 8vw, and never grow larger than 6rem.

The companion functions are just as useful. min() chooses the smaller valid value, which helps with safe widths like min(100% - 2rem, 70rem). max() chooses the larger value, which can protect minimum tap targets, readable spacing, or flexible columns.

Use :has() for Content-Aware Layout

Some cards have images. Some do not. You could add a manual class like project-card--has-image, but then the HTML has to carry layout information that may become stale.

With :has(), CSS can detect whether the card contains an image:

.project-card:has(img) {
  display: grid;
}

@media (min-width: 48rem) {
  .project-card:has(img) {
    grid-template-columns: 1fr 1.4fr;
  }

  .project-card:has(img) img {
    height: 100%;
    object-fit: cover;
  }
}

Now cards without images stay simple, while cards with images can become horizontal on wider screens. This is especially useful for content-managed sites where some entries have optional images and some do not.

Diagram showing a card without an image staying stacked and a card with an image using a media layout when :has(img) matches.
With :has(img), image-specific layout only applies to cards that actually contain an image.

Use Subgrid for Nested Alignment

Cards can look untidy when their internal content does not align. One title wraps over three lines, another uses one line, and suddenly the links sit at different heights like they are avoiding eye contact.

A simple internal grid often solves this:

.project-card {
  display: grid;
  grid-template-rows: auto 1fr;
}

.project-card__content {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
}

The description row can stretch while the link stays lower. That is enough for many card layouts.

Subgrid becomes useful when nested content needs to align with tracks from a parent grid. A nested grid is normally independent; subgrid lets it use the parent grid's row or column tracks.

.project-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  grid-auto-rows: auto;
}

.project-card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 4;
}

Use subgrid when card content, pricing rows, editorial layouts, or feature comparisons need strong shared alignment. Skip it when normal flow, Flexbox, or a simple internal grid already solves the problem.

Comparison of independent card rows with subgrid rows that align card content and actions across columns.
Subgrid is about shared alignment. If nested content needs to line up with the parent grid tracks, subgrid gives those inner pieces the same rhythm.

Helpful rule: if you only need a button to sit at the bottom of a card, use a simple internal grid. Reach for subgrid when nested tracks need to align with parent tracks.

Use minmax() to Protect Layouts

minmax() defines a range for a grid track: a minimum it should not shrink below, and a maximum it can grow toward. You have already used it in the intrinsic grid, but it appears in other useful patterns too:

/* Fixed minimum, flexible maximum */
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));

/* Content-aware sidebar style layout */
grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);

/* Prevent a grid child from forcing overflow */
.main-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

That last minmax(0, 1fr) pattern is small but mighty. It can stop long content from forcing a grid column wider than expected. Tiny line, big "why is this page sideways?" prevention.

Add a Feature Card

Now make one card stand out. The feature class describes a reusable layout role, not one specific project.

<article class="project-card project-card--feature">
  ...
</article>
@media (min-width: 48rem) {
  .project-card--feature {
    grid-column: span 2;
  }

  .project-card--feature:has(img) {
    grid-template-columns: 1fr 1fr;
  }
}

Because the parent grid already uses auto-fit and minmax(), the feature card can span more space when there is room. The :has(img) rule then improves its internal layout only when an image exists.

This is the point of modern CSS layout work: each feature helps the layout make a better decision.

Layout Stress Test

Before you call the layout finished, test it with content that behaves badly. Try:

  • a very long heading
  • a missing image
  • a very short card
  • a very long description
  • browser zoom at 150%
  • narrow mobile width
  • wide desktop width

If the layout still works, you have built something resilient. If it breaks, good. You found the problem before your users did. That is not failure. That is professional CSS.

Final CSS

Here is the complete CSS for the lesson example:

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

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  color: #222;
  background: #f7f4ef;
}

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

a {
  color: currentColor;
  font-weight: 700;
}

.project-section {
  width: min(100% - 2rem, 70rem);
  margin-inline: auto;
  padding-block: clamp(2.5rem, 8vw, 6rem);
}

.section-header {
  max-width: 42rem;
  margin-block-end: clamp(1.5rem, 4vw, 2.5rem);
}

.eyebrow,
.project-card__type {
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.project-grid {
  display: grid;
  gap: clamp(1rem, 3vw, 1.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
}

.project-card {
  display: grid;
  border: 1px solid #ddd3c7;
  border-radius: 1rem;
  overflow: hidden;
  background: #fff;
}

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

.project-card__content {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  gap: clamp(0.75rem, 2vw, 1rem);
  padding: clamp(1rem, 3vw, 1.5rem);
}

.project-card__content > * {
  margin: 0;
}

.project-card__content a {
  align-self: end;
}

@media (min-width: 48rem) {
  .project-card--feature {
    grid-column: span 2;
  }

  .project-card:has(img) {
    grid-template-columns: 1fr 1.4fr;
  }

  .project-card:has(img) img {
    height: 100%;
    aspect-ratio: auto;
  }

  .project-card--feature:has(img) {
    grid-template-columns: 1fr 1fr;
  }
}

Guided Practice

Tune the project card grid

Adjust the card grid, then test whether the CSS still describes a reusable pattern instead of one perfect demo.

Task 1: Change the minimum card width

Find min(18rem, 100%) in the grid columns. Change 18rem to 15rem, then to 22rem.

Watch how the number of columns changes as the minimum card size changes.

💡 Need a hint?
A smaller minimum allows more columns sooner.
A larger minimum protects readability but may create fewer columns.

Task 2: Add a card without an image

Add another project card that has no image. Check that it still looks intentional beside image-based cards.

💡 Need a hint?
The goal is not for every card to be identical. The goal is for every card to look designed.
Use :has(img) so image-specific layout only applies when an image exists.

Task 3: Add an awkward heading

Use this heading: Understanding Layout Decisions When Everything Refuses to Fit Nicely.

Check whether the card still behaves. If it does not, decide whether the grid minimum, card spacing, or text rules need adjustment.

💡 Need a hint?
Long headings are a normal content problem, not a personal attack from the browser.
Test at narrow, medium, and wide widths before deciding on a fix.

Task 4: Move the feature card

Move project-card--feature to a different card. The layout should still work without depending on one exact card.

💡 Need a hint?
This checks whether the class is reusable.
A feature card should describe a layout role, not one specific piece of content.

You are on track if:

  • ☐ You can explain how the minimum card width affects the number of columns
  • ☐ A card without an image still looks intentionally designed
  • ☐ Long headings do not break the layout
  • ☐ The feature-card class can move to another card without rewriting the grid

Independent Practice

💪 Independent Practice: Build a Latest Tutorials grid

Create a new card grid that reuses the same layout thinking with different content.

Your Task:

Create a section called Latest Tutorials. Include four tutorial cards, one featured tutorial, at least one card with no image, and at least one card with a long title.

Requirements:
  • Use a grid with auto-fit and minmax()
  • Use clamp() for responsive spacing or sizing
  • Add one :has() rule for content-aware layout
  • Include one featured card that can move to a different tutorial
  • Test the grid at narrow, medium, and wide widths
Stretch Goals (Optional):
  • Use subgrid where nested alignment actually helps
  • Turn repeated values into simple custom properties for the next architecture lesson

Success Criteria:

CriteriaYou've succeeded if...
Intrinsic gridThe card grid uses auto-fit and minmax() so columns respond to available space without a pile of breakpoint rules.
Fluid rhythmSpacing or sizing uses clamp(), min(), or max() where smooth scaling helps the layout.
Content-aware behaviourAt least one :has() rule changes the layout based on content that is present or missing.
Stress-tested contentThe layout has been checked with long titles, missing images, short cards, narrow widths, and browser zoom.

Lesson Complete: You Can Extend CSS Layouts

Key Takeaways:

  • Modern CSS layout starts with layout decisions, not property memorisation.
  • auto-fit and minmax() let repeated card grids adapt with fewer hard breakpoints.
  • clamp(), min(), and max() help spacing and sizing respond smoothly.
  • :has() lets CSS respond to the structure that is actually present.
  • subgrid is useful when nested grid content needs to align with parent tracks.
  • Stress testing with awkward content is professional CSS, not an optional extra.

Learning Objectives Review:

Look back at what you set out to learn. Can you now:

  • ✅ Choose between Flexbox, Grid, subgrid, and intrinsic sizing patterns Check!
  • ✅ Build responsive card grids with auto-fit and minmax() Got it!
  • ✅ Use clamp(), min(), and max() for fluid spacing and sizing Can explain it!
  • ✅ Use :has() to adjust layout when optional content is present Could teach this!
  • ✅ Explain when subgrid helps nested alignment Check!
  • ✅ Stress-test a layout with awkward real-world content Got it!

If you can confidently answer "yes" to most of these, you're ready to move on!

Think & Reflect:

Layout Decisions

  • Which layout problem in your current project would benefit from intrinsic grid columns?
  • Where are you still using a breakpoint because the layout lacks a flexible default?

Stress Testing

  • Which piece of awkward content revealed the most about your layout?
  • What would you test before reusing this card grid in a later component lesson?

🤔 Real-World Test:

Modern CSS is not about memorising every new feature. It is about choosing the right tool for the layout problem in front of you.

🎯 Looking Ahead:

Next, take this same component idea into Container Queries for Reusable Components, where the card responds to the space it lives in instead of only the browser width.

Recommended Next Steps

Continue Learning

Ready to move forward? Continue with the next tutorial in this series:

BSB Part 4B: Polish and Refine

Additional Resources

Deepen your understanding with these helpful resources:

  • MDN: repeat() - Reference for the repeat() function, including auto-fit and auto-fill patterns used in grid templates.
  • MDN: minmax() - Reference for defining minimum and maximum grid track sizes.
  • MDN: clamp() - Reference for fluid values that stay between a minimum and maximum.
  • MDN: :has() - Reference for selecting an element based on what it contains or relates to.
  • MDN: Subgrid - Guide to sharing parent grid tracks with nested grid layouts.
  • CSS-Tricks: auto-fill vs auto-fit - A visual explanation of the difference between auto-fill and auto-fit in responsive grid columns.
  • CSS-Tricks: The CSS :has Selector - Practical examples of using :has() for content-aware CSS.
  • web.dev: CSS subgrid - A focused explanation of subgrid and where it helps nested layouts align.

Progress tracking is disabled. Enable it in to track your completed tutorials.