Writing Markup That CSS Can Lay Out

Grid and flexbox changed what markup a layout needs. Fewer wrappers, source order that matches reading order, and the reorder trap that breaks keyboard navigation.

Concept

Layout used to demand markup: nested wrappers, clearfix elements, spacer images, rows and columns of divs. CSS grid and flexbox removed almost all of it. The markup a modern layout needs is close to the markup the content needs anyway.

How much has changed

<!-- float era: wrappers, rows, columns, a clearfix -->
<div class="container">
  <div class="row clearfix">
    <div class="col col-8"><div class="inner">Main</div></div>
    <div class="col col-4"><div class="inner">Sidebar</div></div>
  </div>
</div>

<!-- now -->
<div class="layout">
  <main>Main</main>
  <aside>Sidebar</aside>
</div>
.layout {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2rem;
  max-width: 72rem;
  margin-inline: auto;
  padding-inline: 1rem;
}

@media (max-width: 50rem) {
  .layout { grid-template-columns: 1fr; }
}

Two elements instead of seven, and the two that remain are meaningful.

A page level grid

<body>
  <header>...</header>
  <main>...</main>
  <aside>...</aside>
  <footer>...</footer>
</body>
body {
  display: grid;
  grid-template-areas:
    "header header"
    "main   aside"
    "footer footer";
  grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
  gap: 1.5rem;
  min-height: 100vh;
}

header { grid-area: header; }
main   { grid-area: main; }
aside  { grid-area: aside; }
footer { grid-area: footer; }

@media (max-width: 50rem) {
  body {
    grid-template-areas: "header" "main" "aside" "footer";
    grid-template-columns: 1fr;
  }
}

No wrapper divs at all. The semantic elements are the grid items.

The reorder trap

Grid and flexbox can place an item anywhere, regardless of source order. That capability is genuinely dangerous.

/* the sidebar appears first visually, but is second in the source */
aside { order: -1; }

Keyboard focus, screen reader reading order and browser find all follow source order, not visual order. Reorder visually and the two diverge: a keyboard user tabs from the top of the page to something in the middle, then jumps back up.

The Web Content Accessibility Guidelines address this directly - meaningful sequence must be preserved. The rule that follows:

Write the markup in the order it should be read. Use CSS to move things visually only where the reading order genuinely does not matter.

Swapping two cards in a grid is harmless. Moving the main content below a sidebar in the source, then pulling it up with CSS, is not.

Where wrappers are still needed

  • A grid or flex container when the items have no shared meaningful parent.
  • Constraining width when the background must extend full bleed while the content does not.
  • A scroll container around a wide table.
  • Positioning context for an absolutely positioned child.
<!-- full width background, constrained content -->
<section class="band">
  <div class="band-inner">
    <h2>Admissions</h2>
  </div>
</section>
.band { background: #f1f5f9; padding-block: 3rem; }
.band-inner { max-width: 72rem; margin-inline: auto; padding-inline: 1rem; }

Modern CSS can even avoid this one, with a grid that provides its own gutters - but the wrapper is clear and costs little.

Card markup that works

<article class="card">
  <img src="/images/design.jpg" alt="" width="600" height="400">
  <div class="card-body">
    <h3><a href="/courses/design">Design</a></h3>
    <p>Three years, studio based.</p>
  </div>
  <footer class="card-meta">
    <p>Sixty places</p>
  </footer>
</article>
.card {
  display: grid;
  grid-template-rows: auto 1fr auto;   /* image, body grows, footer pinned */
  border: 1px solid #cbd5e1;
  border-radius: 10px;
  overflow: hidden;
}
.card img { width: 100%; height: auto; object-fit: cover; }
.card-body { padding: 1rem; }

The 1fr middle row makes every card in a row the same height with the footers aligned, which used to require JavaScript.

Making a whole card clickable

.card { position: relative; }

.card h3 a::after {
  content: "";
  position: absolute;
  inset: 0;          /* stretch over the whole card */
}

One real link, one tab stop, sensible link text, and the entire card is a click target. Far better than wrapping everything in a link with no meaningful text, or attaching a click handler to the card.

Layout utilities without wrappers

/* consistent vertical rhythm between siblings */
.flow > * + * { margin-top: 1rem; }

/* a responsive grid with no media query */
.auto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1.5rem;
}

/* centre with a maximum width */
.wrapper { max-width: 72rem; margin-inline: auto; padding-inline: 1rem; }

Important rules

  • Source order is reading order, keyboard order and find order.
  • Grid and flex properties apply to direct children only, so one wrapper level is sometimes unavoidable.
  • gap works in both grid and flexbox and replaces margin based gutters.
  • A grid item defaults to min-width: auto, which lets long content overflow. minmax(0, 1fr) fixes it.
  • Changing display does not change what an element means.

Common mistakes

  • Carrying float era wrapper chains into a grid based project.
  • Reordering the main content visually and breaking keyboard navigation.
  • A grid item overflowing because of the default minimum size.
  • Wrapping a card in a link with no accessible text.
  • Adding a wrapper for every level of styling instead of using gap and pseudo elements.
  • Using section as a layout wrapper where div is correct.

Best practices

  • Write the markup for the content, then lay it out with grid.
  • Keep source order equal to reading order.
  • Use gap rather than margins for gutters.
  • Use minmax(0, 1fr) for grid columns holding unpredictable content.
  • Prefer the stretched link pseudo element for clickable cards.
  • Tab through every layout after building it.

Practice

  1. Rebuild a float based three column layout as a grid and count the elements you removed.
  2. Reorder a sidebar visually with order, then tab through the page and describe the problem.
  3. Build a card grid where every card is the same height with aligned footers, using no JavaScript.
  4. Make a card fully clickable with a stretched link and confirm it is one tab stop.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All HTML notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.