Full-Bleed Sections in a Constrained Layout: Breaking Out Without Breaking the Page

Long-form pages keep text in a comfortable measure — 60 to 75 characters — centred in the viewport. Then design asks for a photo that runs edge to edge, a coloured quote band, or a wide data table that needs more room than the text column. The obvious fix, width: 100vw with a negative margin, works until a Windows user with classic scrollbars opens the page and finds a horizontal scrollbar the width of the vertical one. This page shows the grid-based alternative that needs no viewport units at all, explains exactly why 100vw misbehaves, and covers a middle "breakout" width for content that should be wider than text but not full-bleed. It sits in Viewport Units & Mobile Layout within the Mastering Container Queries & Responsive Layouts guide.

Why the obvious approach leaks

The negative-margin technique is clever. A child of a centred column sets width: 100vw and margin-inline: calc(50% - 50vw). The margin is negative by half the difference between the viewport and the column, so the child extends equally past both edges of the column to the viewport's edges.

The flaw is in vw. One vw is one percent of the initial containing block width, which on desktop platforms with space-taking scrollbars includes the scrollbar gutter. The page's content area is narrower than 100vw by the scrollbar's width, typically 15 to 17 pixels. A 100vw element therefore overhangs the right edge by that amount, and the document gains a horizontal scrollbar. On macOS and mobile, where scrollbars overlay content, the bug is invisible — which is why it ships so often.

100vw includes the scrollbar A window frame whose content area stops at a vertical scrollbar. A band sized to 100vw extends underneath the scrollbar and past the content edge, causing a horizontal scrollbar at the bottom. The overhang is exactly one scrollbar wide text column width: 100vw text column band runs under the scrollbar → horizontal scroll

The usual patch — overflow-x: hidden on body — hides the symptom and introduces two new problems: it clips anything legitimately overflowing, and it breaks position: sticky for descendants, because sticky positioning is relative to the nearest scrolling ancestor and overflow: hidden makes body that ancestor.

The complete implementation

The grid method turns the article into a three-column grid. The side columns are flexible gutters; the middle column is the reading measure. Every child goes into the middle column by default, and a child that should bleed simply spans all three.

Live demoFull-bleed, breakout and content columns from named lines
Narrow the frame: gutters shrink first, then the breakout columns, and the full-bleed band always reaches both edges with no horizontal scroll. 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>Full-bleed grid</title>
<style>
  html, body { margin: 0; }
  body { font: 17px/1.65 Georgia, serif; }

  .article {
    display: grid;
    /* Five columns with named lines:
       [full] gutter [breakout] extra [content] measure [content] extra [breakout] gutter [full]
       The gutters never shrink below 1rem, so text never touches the edge. */
    --gutter: 1rem;
    --measure: 65ch;
    --breakout-extra: 8rem;
    grid-template-columns:
      [full-start] minmax(var(--gutter), 1fr)
      [breakout-start] minmax(0, var(--breakout-extra))
      [content-start] min(var(--measure), 100% - 2 * var(--gutter))
      [content-end] minmax(0, var(--breakout-extra))
      [breakout-end] minmax(var(--gutter), 1fr)
      [full-end];
  }

  /* Default placement: every child sits in the reading column. */
  .article > * { grid-column: content; }

  /* Wider than text, narrower than the page: tables, code, figures. */
  .article > .breakout { grid-column: breakout; }

  /* Edge to edge. No viewport units, so no scrollbar overhang. */
  .article > .full-bleed { grid-column: full; }

  .full-bleed img { display: block; width: 100%; height: auto; }

  /* A band whose background bleeds but whose text lines up with the
     column above: it re-declares the same grid for its own children. */
  .band {
    grid-column: full;
    display: grid;
    grid-template-columns: inherit;
    padding-block: 2rem;
    background: #1e293b;
    color: #f1f5f9;
  }
  .band > * { grid-column: content; }
</style>
</head>
<body>
  <article class="article">
    <h1>Crossing the Plateau</h1>
    <p>Reading text stays in a comfortable measure.</p>
    <figure class="full-bleed">
      <img src="/img/plateau.jpg" alt="Wide view of a grassy plateau under storm clouds" width="1600" height="700">
    </figure>
    <p>More reading text, back in the column.</p>
    <table class="breakout"><tr><th>Day</th><th>Distance</th></tr><tr><td>1</td><td>18 km</td></tr></table>
    <aside class="band"><p>A pull quote whose background runs edge to edge.</p></aside>
  </article>
</body>
</html>

The grid-template-columns: inherit in the band is the tidy trick. The band spans the full width, becomes a grid itself, copies its parent's column template, and places its own children in the same content column — so the quote text lines up exactly with the paragraphs above and below while the dark background runs to both edges. In engines with subgrid, grid-template-columns: subgrid achieves the same with a live link to the parent's tracks rather than a copy.

The key technique: named line pairs

The whole approach rests on a grid feature that is easy to overlook. When you name lines content-start and content-end, the grid automatically creates an implicit named area called content, and grid-column: content means "from content-start to content-end". Naming three nested pairs creates three nested areas, and choosing a width for a child becomes choosing a name.

Three nested areas from named lines Five columns across the page. Content spans the middle column, breakout spans the middle three, and full spans all five. Each area is created implicitly from a start and end line pair. grid-column: full | breakout | content gutter extra measure extra gutter content breakout full Naming x-start and x-end lines creates an area called x automatically.

The column sizes are chosen so the layout degrades gracefully. On a wide screen, the measure is 65ch, the breakout columns reach their 8rem maximum, and the gutters absorb the remaining width. As the viewport narrows, the gutters shrink first, then the breakout columns shrink toward zero, and finally the measure itself shrinks — min(var(--measure), 100% - 2 * var(--gutter)) keeps it from ever exceeding the space left after two minimum gutters. On a phone, content, breakout and full-bleed text all end up the same width with 1rem gutters, while full-bleed images still reach both edges.

Choosing the three widths

The custom properties at the top of the grid are design decisions, and they are worth making deliberately rather than copying.

The measure controls readability. The ch unit is the width of the "0" glyph in the current font, so 65ch is roughly 65 characters of body text — the middle of the range typographers recommend for comfortable reading. Because it is font-relative, the column widens automatically if the reader increases their default font size, which keeps the character count per line stable under zoom. A measure in pixels or rem does not have that property. If body text is set with fluid typography, the measure follows the fluid size too.

The breakout extra should be large enough that breakout content looks intentionally wider, not accidentally misaligned. Somewhere between 4rem and 10rem per side works for most layouts. Code blocks are the usual beneficiary: a 65ch prose column is too narrow for an 80-column code sample, while a breakout of 65ch plus 16rem fits one comfortably.

The gutter is the minimum distance from the viewport edge to any text. It should match your mobile page padding — commonly 1rem to 1.5rem — so that on a phone the article has the same edge spacing as every other page. Since it is a minmax() floor, it has no effect on wide screens, where the flexible 1fr tracks take over.

Keeping these as custom properties on the article means a single page type — a photo essay, say — can widen its measure or breakout with one override instead of a new grid definition.

Variation: full-bleed inside a nested container

Real pages rarely put every block directly inside the article grid. A CMS wraps content in a <div class="prose">, or a component library wraps a figure in its own element. The grid method only works for direct children of the grid, so nested full-bleed needs one of two approaches.

Two ways to bleed from inside a wrapper Left, the wrapper uses subgrid so the nested figure can still address the full line names. Right, the nested figure uses a negative margin calculated from the wrapper's width, which only works if the wrapper is centred. When the full-bleed child is not a direct child Subgrid wrapper .prose { grid-template-columns: subgrid } grid-column: full lines pass through; no maths Negative margins centred wrapper calc(50% - 50cqi) works only if wrapper is centred

With subgrid, the wrapper spans full itself and declares grid-template-columns: subgrid, which adopts the parent's tracks and their line names. Its children can then use grid-column: content or full exactly as if they were direct children. This is the cleanest option when you control the wrapper's CSS.

When you do not, the negative-margin method can be rescued from the scrollbar bug by replacing vw with a container unit. Make the page's scrolling root a size container (body { container-type: inline-size; }), and use margin-inline: calc(50% - 50cqi) with width: 100cqi. Container query units measure the container's content box, which excludes the scrollbar, so the overhang disappears. The trade-offs of that switch — including why a size container on body is safe here but not everywhere — are laid out in Viewport Units vs Container Units.

On phones with notches, full-bleed images should run under the safe areas while the band text should not; add padding-inline: max(1rem, env(safe-area-inset-left)) max(1rem, env(safe-area-inset-right)) to text inside full-bleed bands, as described in Safe Area Insets for Notched Screens.

Browser support

CSS Grid with named lines and minmax() is supported in Chrome 57+, Edge 16+, Firefox 52+ and Safari 10.1+, and min() inside a track size in Chrome and Edge 79+, Firefox 75+ and Safari 13.1+. subgrid is supported in Chrome and Edge 117+, Firefox 71+ and Safari 16+ — Firefox shipped it years before the others. Container query units, used in the rescued negative-margin variant, are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. Without subgrid, the grid-template-columns: inherit copy shown in the band works in every grid-capable browser.

FAQ

Why does width: 100vw cause a horizontal scrollbar?vw units include the vertical scrollbar's width on platforms where scrollbars take up space. An element 100vw wide is therefore wider than the area inside the scrollbar, and the page gains a horizontal scrollbar a few pixels wide. The grid method avoids viewport units entirely.

What is the named-lines grid technique for full-bleed layouts? The article becomes a three-column grid: flexible gutters on both sides and a content column in the middle, with line names such as full-start, content-start, content-end and full-end. Normal children go in the content column; full-bleed children span from full-start to full-end.

Can a full-bleed element be nested inside another container? With the grid method, only if every ancestor between it and the full-width grid passes the lines down, which subgrid can do. With the negative-margin method it can break out of any centred ancestor, but it depends on that ancestor being centred in the viewport.

How do I keep text inside a full-bleed section aligned with the text column? Make the full-bleed section itself a grid with the same column template, or give it subgrid columns. Its inner content then sits in the same content column as the surrounding prose, while its background runs edge to edge.

Related articles

More pages in the same section.