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.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
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.
gapworks 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
displaydoes 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
gapand pseudo elements. - Using
sectionas a layout wrapper wheredivis correct.
Best practices
- Write the markup for the content, then lay it out with grid.
- Keep source order equal to reading order.
- Use
gaprather 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
- Rebuild a float based three column layout as a grid and count the elements you removed.
- Reorder a sidebar visually with
order, then tab through the page and describe the problem. - Build a card grid where every card is the same height with aligned footers, using no JavaScript.
- Make a card fully clickable with a stretched link and confirm it is one tab stop.