Content Grid
Inspired by Kevin Powell's content grid technique, based on original ideas from Stephanie Eckles (smolcss.dev) and Ryan Mulligan.
The .content-grid class eliminates the need for wrapper or container divs. Instead of nesting content inside <div class="container"> inside each <section>, you put a single .content-grid on your <main> (or any top-level element) and use mini-classes to control width at the element level.
The grid provides three width levels via three CSS Grid named lines:
| Level | Grid area | Class | Width |
|---|---|---|---|
| Content | content | (none — default) | Constrained to --content-grid-content-max-width (default 70ch) |
| Breakout | breakout | .breakout | Wider than content, up to --content-grid-breakout-max-width (default 90ch) |
| Full-width | full-width | .full-width | Edge-to-edge spanning the entire viewport |
Note: Some of the examples below use utility classes. In case you are using the
no-utilitiesbundle, be aware that these classes won't be available.
Basic Usage
Apply .content-grid to your <main> (or <body>, or any wrapper). All direct children are automatically placed in the content column:
<main class="content-grid">
<hgroup>
<h1>Page title</h1>
<p>This paragraph stays within the content column automatically.</p>
</hgroup>
<p>Every direct child is constrained to <code>--content-grid-content-max-width</code>.</p>
</main>Breakout
Use .breakout on an element that should be wider than the default content column but not the full viewport — great for pull quotes, wider images, or call-to-action sections:
<main class="content-grid">
<hgroup>
<h1>Article Title</h1>
<p>Regular content stays constrained.</p>
</hgroup>
<blockquote class="breakout">
This pull quote breaks out of the content column into the breakout area —
noticeably wider, but still with padding on each side.
</blockquote>
<p>Back to regular content width.</p>
</main>Full-width
Use .full-width on an element that should span edge-to-edge — sections with background colours, hero banners, full-bleed images:
<main class="content-grid">
<hgroup>
<h1>Top Level section</h1>
<p>This section is the one that started the content grid.</p>
</hgroup>
<section class="full-width pane primary">
<hgroup>
<h2>Full-width section</h2>
<p>This section spans edge-to-edge, but the text inside is still constrained
to the content column because <code>.full-width</code> creates a nested grid.</p>
</hgroup>
<p>You can even use <code>.breakout</code> inside a <code>.full-width</code>.</p>
<blockquote class="breakout">
This pull quote breaks out of the content column into the breakout area —
noticeably wider, but still with padding on each side.
</blockquote>
</section>
<p>Back to normal content flow.</p>
</main>How full-width works
.full-width spans the entire viewport via grid-column: full-width and simultaneously creates a nested content grid with grid-template-columns: inherit. This means:
- Its background fills the full width (great for section backgrounds)
- Its children are still automatically constrained to the content column
.breakoutand.full-widthwork inside it as well
Nested full-width inside full-width
A .full-width element creates a nested content grid, so you can place another .full-width inside it and it will span edge-to-edge within the parent — useful for alternating background bands nested inside a larger section:
<main class="content-grid">
<hgroup>
<h1>Top Level section</h1>
<p>This section is the one that started the content grid.</p>
</hgroup>
<div role="status" class="breakout">
This alert message breaks out of the content column into the breakout area —
</div>
<section class="full-width pane primary">
<hgroup>
<h2>Outer full-width section</h2>
<p>This section spans edge-to-edge, but the text inside is still constrained
to the content column because <code>.full-width</code> creates a nested grid.</p>
</hgroup>
<p>You can even nest another <code>.full-width</code> inside a <code>.full-width</code>.</p>
<section class="full-width pane">
<hgroup>
<h3>Nested full-width section</h3>
<p>This inner ".full-width" also spans edge-to-edge within the outer one,
and its children are still constrained to the content column.</p>
</hgroup>
<p>Unless the parent has padding.</p>
</section>
<section class="breakout pane contrast">
This pane breaks out wider than the surrounding content.
</section>
<p>Back to the outer section's content column.</p>
</section>
<p>Back to the top-level content column.</p>
</main>The inner .full-width receives grid-column: full-width on the nested grid, so its background fills the entire width of the parent. Its children are still automatically constrained to the content column, and you can use .breakout or further .full-width children inside it.
Responsive Behavior
The content grid is fully responsive with zero media queries. It works at every viewport size:
- Wide viewport: content is constrained to
--content-grid-content-max-width, breakout extends past it - Medium viewport: breakout columns shrink; content and breakout converge
- Narrow viewport: breakout columns collapse to zero;
.breakoutcontent sits at the same width as regular content
This is powered by minmax() for the side columns and min() for the content column:
/* Side padding columns — at minimum padding-inline, can grow */
minmax(var(--content-grid-padding-inline), 1fr)
/* Breakout columns — can shrink to 0 on small screens */
minmax(0px, var(--content-grid-breakout-size))
/* Content column — chooses the smaller of max-width vs available space */
min(var(--content-grid-content-max-width), 100% - var(--content-grid-padding-inline) * 2)CSS Custom Properties
Customise the layout by overriding these variables on any .content-grid element:
| Variable | Default | Description |
|---|---|---|
--content-grid-padding-inline | var(--spacing) | Minimum padding on each side (between viewport edge and content) |
--content-grid-content-max-width | 70ch | Maximum width of the content column |
--content-grid-breakout-max-width | 90ch | Maximum total width when using .breakout |
Example: Tighter layout
.content-grid {
--content-grid-padding-inline: 1rem;
--content-grid-content-max-width: 60ch;
--content-grid-breakout-max-width: 80ch;
}Example: Wide reading layout
.content-grid {
--content-grid-content-max-width: 80ch;
--content-grid-breakout-max-width: 110ch;
}You can also set these inline or scope them to a specific region:
<div class="content-grid" style="--content-grid-content-max-width: 60ch;">
...
</div>How It Works
The technique relies on CSS Grid named lines. The grid template defines five columns bracketed by named lines that follow the {name}-start / {name}-end pattern, which creates implicit named grid areas:
┌─────────────┬────────────┬──────────────────┬────────────┬─────────────┐
│ full-width │ breakout │ content │ breakout │ full-width │
│ padding │ column │ column │ column │ padding │
├─────────────┼────────────┼──────────────────┼────────────┼─────────────┤
│ minmax( │ minmax( │ min(max-width, │ minmax( │ minmax( │
│ padding, │ 0, size) │ 100%-padding*2) │ 0, size) │ padding, │
│ 1fr) │ │ │ │ 1fr) │
└─────────────┴────────────┴──────────────────┴────────────┴─────────────┘
[full- [breakout- [content- [content- [breakout- [full-
width-start] start] start] end] end] width-end]Any child can target one of these areas by setting grid-column to its name:
| CSS | Spans |
|---|---|
grid-column: content | Just the content column |
grid-column: breakout | From breakout-start to breakout-end (content + both breakout columns) |
grid-column: full-width | From full-width-start to full-width-end (everything) |
The framework sets grid-column: content on all direct children by default (excluding .breakout and .full-width), so regular content is automatically constrained.