Responsive Hero Sections With Container Queries

The hero banner is the most visible component on a landing page and one of the most reused: the same headline-image-button arrangement appears full width on the homepage, half width in a campaign column, and narrow in a promotional sidebar or an upgrade modal. A hero designed with viewport media queries looks right in exactly one of those places. The narrow problem this page solves is a hero that chooses its layout from its own width — stacked when narrow, split side by side when medium, text overlaid on a full-bleed image when wide — while keeping text readable over photography and its call to action visible on mobile. It belongs to Responsive Component Patterns in the Mastering Container Queries & Responsive Layouts guide.

Why the hero must measure itself

A viewport breakpoint encodes an assumption: "at 1024 pixels wide the hero has room for a split layout". That is true when the hero spans the page and false when the same component sits in a 480-pixel column of that 1024-pixel page. The failure is visible and embarrassing — a headline squeezed to three words per line beside an image — and it happens on exactly the pages where marketing teams reuse the component in new contexts.

With the hero as a size container, the thresholds describe the hero itself: "at 40rem of hero width, go side by side". Every placement then gets the layout that fits it.

Three layouts, chosen by the hero's own width Narrow: the image sits above the headline, text and button. Medium: image and text sit side by side in two columns. Wide: the image fills the whole hero and the text sits over its left side on a gradient scrim. Stacked · split · overlay < 40rem 40–64rem >= 64rem

The complete implementation

Resize the demo through all three layouts. The image is an <img> with object-fit: cover, so it crops cleanly at every aspect ratio.

Live demoHero that stacks, splits or overlays by its own width
Drag the frame: below 40rem the image stacks above the text, between 40 and 64rem they split, and above 64rem the text overlays the image on a scrim. Drag the bottom-right corner to resize the frame in either direction.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Container-driven hero</title>
<style>
  body { margin: 0; font: 16px/1.5 system-ui, sans-serif; }

  .hero-slot { container: hero / inline-size; }

  .hero {
    display: grid;
    gap: 1.25rem;
    padding: clamp(1rem, 4cqi, 2.5rem);
    background: #0f172a;
    color: #f8fafc;
  }

  .hero__media img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: 12px;
  }

  .hero__title {
    margin: 0;
    font-size: clamp(1.6rem, 1rem + 4cqi, 3.25rem);   /* scales with the hero */
    line-height: 1.1;
    text-wrap: balance;
  }
  .hero__lede { margin: 0.75rem 0 1.25rem; max-width: 45ch; color: #cbd5e1; }
  .hero__cta {
    display: inline-block;
    padding: 0.7rem 1.2rem;
    border-radius: 8px;
    background: #facc15;
    color: #1c1917;
    font-weight: 700;
    text-decoration: none;
  }
  .hero__cta:focus-visible { outline: 3px solid #f8fafc; outline-offset: 3px; }

  /* SPLIT: media and text side by side. */
  @container hero (40rem <= width < 64rem) {
    .hero { grid-template-columns: 1fr 1fr; align-items: center; }
    .hero__media img { aspect-ratio: 4 / 5; }
  }

  /* OVERLAY: the image fills the hero, text sits on a scrim. */
  @container hero (width >= 64rem) {
    .hero {
      grid-template-areas: "stack";
      padding: 0;
      min-height: min(36rem, 100svh);
    }
    .hero > * { grid-area: stack; }
    .hero__media img { height: 100%; aspect-ratio: auto; border-radius: 0; }
    .hero__text {
      z-index: 1;
      align-self: center;
      max-width: 34rem;
      padding: clamp(1.5rem, 4cqi, 3.5rem);
    }
    /* Scrim: darkest behind the text, fading toward the image's right. */
    .hero::after {
      content: "";
      grid-area: stack;
      background: linear-gradient(90deg, rgb(15 23 42 / 0.92) 0%, rgb(15 23 42 / 0.6) 45%, transparent 75%);
    }
  }
</style>
</head>
<body>
  <div class="hero-slot">
    <section class="hero" aria-labelledby="hero-title">
      <div class="hero__media">
        <img src="/img/ridge.jpg" alt="Hikers crossing a ridge at sunrise" width="1600" height="900">
      </div>
      <div class="hero__text">
        <h1 class="hero__title" id="hero-title">Walk the ridge before the crowds arrive</h1>
        <p class="hero__lede">Guided dawn hikes with small groups and no rush.</p>
        <a class="hero__cta" href="/book/">Book a dawn hike</a>
      </div>
    </section>
  </div>
</body>
</html>

In the overlay layout all three layers — image, scrim and text — occupy the same grid area, named stack, and paint in order: the image first, the scrim (::after) above it, and the text on top via z-index. Stacking with a single grid area avoids absolute positioning, so the hero's height is still determined by its content and min-height, not by guesses. The technique is developed in Overlapping Elements With Grid Stacking.

The key technique: contrast against the worst pixel

Text on photography fails contrast in the brightest part of the image, not on average. A white headline over a mostly dark photo with a bright sky behind the first line fails exactly where it matters. The scrim guarantees a minimum darkness behind the text regardless of the image.

Contrast measured along the headline A plot of text contrast ratio across the width of the text block. Without a scrim, contrast falls below the 4.5 to 1 threshold over a bright patch of sky. With a scrim darkest on the left, the curve stays above 4.5 across the whole text width. White text contrast across the text block 4.5 no scrim: fails over bright sky with scrim: stays above 4.5 horizontal position across the text →

The gradient in the example is darkest at the left edge, where the text starts, and transparent by three-quarters of the way across, so most of the photograph stays visible. Check the result against the lightest image the hero will realistically use; for heroes whose images are chosen by editors, test with a deliberately bright image. The contrast requirement — 4.5:1 for normal text and 3:1 for large text, per WCAG 1.4.3 — applies to the call-to-action button's label too, which is why it uses dark text on a bright yellow fill rather than relying on the scrim.

Loading performance

The hero image is usually the largest element in the first screen, which makes it the page's Largest Contentful Paint candidate — the Core Web Vitals metric for how quickly the main content appears. Three decisions in the markup affect it directly.

Do not lazy-load it. loading="lazy" on an above-the-fold image delays its request until layout has confirmed it is in view, which postpones the largest paint. Leave the hero image eager, and consider fetchpriority="high" so the browser requests it ahead of less important resources.

Serve sizes that match the layouts. The same hero is 100% wide stacked, 50% wide split and 100% wide overlaid. A sizes attribute describing those widths — expressed against the viewport, because sizes cannot see container widths — lets the browser choose an appropriate file from srcset for each placement. Where a component appears in placements the sizes value cannot predict, err toward slightly larger files rather than blurry ones.

Reserve the space. width and height attributes plus aspect-ratio in CSS mean the hero's box is correct before the image arrives, so nothing below it shifts. In the overlay layout, where the image fills a min-height box, the height is fixed by CSS and the attributes still provide the intrinsic ratio for the stacked and split layouts.

Motion and the hero

Heroes attract entrance animations: the headline sliding in, the image slowly zooming. Keep them restrained. A large headline moving across the screen on load is precisely the kind of motion that affects people with vestibular disorders, and a slowly zooming background image is a looping animation that WCAG 2.2.2 asks you to make pausable if it lasts more than five seconds. A short fade of the text and a single, subtle scale of the image are enough, gated by prefers-reduced-motion; the patterns are in Scroll-Triggered Reveal Animations and Vestibular-Safe Animation Patterns.

The hero's height also interacts with mobile browsers. min-height: min(36rem, 100svh) caps the overlay hero at the small viewport height, so on a phone in landscape its call to action stays visible without scrolling. The reasoning behind svh over vh is covered in dvh, svh and lvh.

Variation: text below in the overlay layout on short containers

Very wide but short placements — a banner strip across a dashboard — have room for the overlay layout horizontally but not enough height for text over the image. A size container can query height as well as width to choose the split layout instead.

.hero-slot {
  container: hero / size;
  block-size: var(--hero-height, auto);
}

@container hero (width >= 64rem) and (height < 20rem) {
  .hero { grid-template-areas: none; grid-template-columns: 2fr 3fr; }
  .hero > * { grid-area: auto; }
  .hero::after { content: none; }
}

Size containment requires the slot to have an explicit block size, which is why this variation reads it from a custom property that each placement sets. For most heroes, inline-size containment and a min-height are simpler; reach for size containment only when placements genuinely vary in height.

Browser support

Container queries and container query units are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. svh is supported in Chrome and Edge 108+, Firefox 101+ and Safari 15.4+. aspect-ratio is supported in Chrome and Edge 88+, Firefox 89+ and Safari 15+. text-wrap: balance is supported in Chrome and Edge 114+, Firefox 121+ and Safari 17.5+.

FAQ

Should a hero use a container query or a media query? A container query, if the hero is a reusable component. Landing pages place heroes full width, but the same component also appears in narrower contexts such as a campaign column, a modal or a CMS preview pane. A container query rearranges it for the space it actually has.

How do I keep text readable over a hero image? Place a gradient scrim between the image and the text, darkest behind the text, so contrast meets 4.5:1 regardless of the photo. Check contrast against the lightest part of the image under the text, not the average.

What height should a hero be on mobile? Tall enough for its content, and at most the small viewport height so its call to action is visible on load. min-height with svh achieves that; avoid fixed heights in vh, which on mobile include the space behind the browser toolbar.

Should the hero image be a background-image or an img element? Use an img element with object-fit: cover when the image is content or needs responsive sources, alt text and lazy-loading control. Use a CSS background only for purely decorative images.

Related articles

More pages in the same section.