Skip to content

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:

LevelGrid areaClassWidth
Contentcontent(none — default)Constrained to --content-grid-content-max-width (default 70ch)
Breakoutbreakout.breakoutWider than content, up to --content-grid-breakout-max-width (default 90ch)
Full-widthfull-width.full-widthEdge-to-edge spanning the entire viewport

Note: Some of the examples below use utility classes. In case you are using the no-utilities bundle, 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:

live preview
editable html
<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:

live preview
editable html
<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:

live preview
editable html
<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
  • .breakout and .full-width work 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:

live preview
editable html
<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; .breakout content sits at the same width as regular content

This is powered by minmax() for the side columns and min() for the content column:

css
/* 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:

VariableDefaultDescription
--content-grid-padding-inlinevar(--spacing)Minimum padding on each side (between viewport edge and content)
--content-grid-content-max-width70chMaximum width of the content column
--content-grid-breakout-max-width90chMaximum total width when using .breakout

Example: Tighter layout

css
.content-grid {
  --content-grid-padding-inline: 1rem;
  --content-grid-content-max-width: 60ch;
  --content-grid-breakout-max-width: 80ch;
}

Example: Wide reading layout

css
.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:

html
<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:

CSSSpans
grid-column: contentJust the content column
grid-column: breakoutFrom breakout-start to breakout-end (content + both breakout columns)
grid-column: full-widthFrom 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.

References