Appearance
6.2.1 — HTML Semantics & Content Models
Write this and look at what the browser built:
html
<p>
Order summary
<div>Total: £42.00</div>
</p>The elements panel shows three siblings, not a nesting:
html
<p>Order summary</p>
<div>Total: £42.00</div>
<p></p>Your <div> is no longer inside the paragraph, and an empty paragraph has appeared out of nowhere. No error was reported. Every CSS rule you wrote for p div matches nothing, and every stylist who has hit this has spent twenty minutes blaming the CSS.
This page is about the rule that caused it, and about the larger idea behind that rule: HTML elements are not interchangeable containers. Each one declares what it means, and the specification says exactly which meanings can nest inside which.
1. Content models: the rule the parser is enforcing
Every HTML element belongs to one or more content categories, and every element's definition states which categories it will accept as children. That pairing is called its content model.
The categories you need are these six:
| Category | What is in it | Example elements |
|---|---|---|
| Metadata | Things about the document | title, meta, link, style |
| Flow | Almost everything in the body | div, p, table, section, img |
| Phrasing | Text and things that mark up text | span, a, strong, em, img, code |
| Embedded | Content from elsewhere | img, video, iframe, canvas, svg |
| Interactive | Things a user operates | a (with href), button, input, select |
| Sectioning | Things that create an outline entry | article, section, nav, aside |
Categories overlap on purpose. An <img> is phrasing and embedded and flow, which is why it can sit in a paragraph as well as on its own.
Now the specific rule from the opening example: <p> has a content model of "phrasing content". A <div> is flow content and not phrasing content, so it is not allowed there.
What the parser does with the violation is the part that trips people up. It does not throw. Following Chapter 6.1.2's rule that HTML parsing never fails, it applies the specified recovery: seeing a start tag that cannot be a child, it closes the open element first. So <p> closes, <div> becomes its sibling, and then the </p> you wrote later has no matching open paragraph — so the parser opens a fresh empty one to close. That is where the stray <p></p> came from.
The general principle to carry away: when nesting looks wrong in the elements panel, you have violated a content model, and the parser silently repaired it. The repair is deterministic and identical in every browser, which is a feature, but it means the failure is silent.
The nestings that catch people, and what happens instead
<div>, <ul>, <table> or another <p> inside a <p>. The paragraph closes early, exactly as above. Any block-level element does this.
<a> inside <a>. The inner one closes the outer. This one appears in real code when a whole card is wrapped in a link and someone adds a "Read more" link inside it. Interactive content cannot nest inside interactive content, for the obvious reason that a click would be ambiguous.
<button> containing a link, or <label> containing another <label>. Same category, same problem.
<form> inside <form>. The inner one is dropped entirely. Real consequence: a search box nested inside a checkout form does not submit the search, it submits the checkout.
Anything other than <li> as a direct child of <ul>. A <div> wrapper around list items is not allowed, and the parser moves it before the list. Your rows end up above the list rather than inside it.
A <tr> without a <tbody>. This one goes the other way: the parser inserts an element you did not write. Every table row is placed inside an implied <tbody>, so table > tr as a CSS selector matches nothing while table > tbody > tr matches everything. This is the second-most-common "my CSS does not apply" mystery after the paragraph one.
Anything at all inside a void element such as <img>, <br>, <input>, <hr>, <meta>. These have no closing tag and no children by definition, and writing </img> is simply ignored.
One check that costs nothing: the W3C validator, or the HTML lint rule in your editor, catches every one of these before it becomes a mystery. The failures are silent at runtime precisely because they are meant to be caught before runtime.
2. What semantic elements actually do
"Use semantic HTML" is repeated so often that it has stopped carrying information. Here is the concrete version: the browser builds a second tree next to the DOM, called the accessibility tree, and the element you choose determines what goes in it.
Every node in that tree carries three things:
- a role — what kind of thing this is (button, link, heading, list, navigation region)
- a name — the text that identifies it ("Add to basket")
- states and properties — pressed, expanded, disabled, checked, required, the current value
Screen readers, voice control, browser reader modes, search engine crawlers and automated testing tools all read this tree, not your CSS. A <div> contributes a node with no role, no name and no states. A <button> contributes a node with role=button, the name taken from its text, and a state that tracks whether it is disabled.
That is the whole argument, and it is why the two snippets below are not equivalent no matter how identical they look:
html
<!-- What the accessibility tree sees: a generic node with some text. -->
<div class="btn" onclick="addToBasket()">Add to basket</div>
<!-- What the accessibility tree sees: button, named "Add to basket". -->
<button type="button" onclick="addToBasket()">Add to basket</button> The <div> version also loses four behaviours you would have to rebuild by hand: it is not focusable by keyboard, Enter and Space do not activate it, it is not announced as an interactive control, and it does not participate in form submission. Rebuilding those means tabindex="0", a keydown handler that checks two keys, role="button", and manual aria-disabled management — four extra pieces of code to reproduce something you got for free.
Drawn side by side, the difference is the whole point of choosing elements carefully:
The rule of thumb that follows: use the native element, and reach for ARIA attributes only when there is no native element for what you are building. The first rule of ARIA, in the specification itself, is not to use ARIA.
The document landmarks
These elements exist to divide a page into regions that assistive technology can jump between, the way a sighted user's eye jumps to the header:
html
<body>
<header> <!-- banner: masthead, logo, site-wide search -->
<nav>…</nav> <!-- navigation: a set of links to elsewhere -->
</header>
<main> <!-- the unique content of THIS page. Exactly one. -->
<article> <!-- self-contained: makes sense syndicated alone -->
<h1>…</h1>
<section>…</section> <!-- a thematic chunk WITH a heading -->
</article>
<aside>…</aside> <!-- tangentially related: related links, a pull quote -->
</main>
<footer>…</footer> <!-- contentinfo: copyright, site-wide links -->
</body>Two of these deserve a note because they are the ones misused.
<main> should appear exactly once and must contain what is unique to this page. The header, the site navigation and the footer are repeated on every page and belong outside it. A screen-reader user's first action on an unfamiliar page is frequently "jump to main", which skips the fifty navigation links they have already heard on the previous page.
<section> is not a <div> with better manners. It means "a thematic grouping, normally with a heading". If there is no heading that would sensibly go inside it, it should be a <div>. A <section> with no heading contributes a nameless region to the accessibility tree, which is noise rather than structure. <div> remains completely correct for grouping things for layout purposes — that is precisely its job, and using it is not a failure.
Headings, and the outline that never happened
Headings <h1> through <h6> create the document's structure, and structure is the thing screen-reader users navigate by: a keystroke moves to the next heading, and a shortcut lists all of them as a table of contents.
Two rules, both of which are commonly broken:
Do not skip levels going down. <h1> then <h3> reads as a missing level and breaks the mental map. Coming back up is fine — <h3> followed by <h2> just means a new section started.
Choose the level by position in the outline, not by how big you want the text. Font size is CSS. If your <h2> should look small, style it.
There is a piece of folklore worth correcting here. HTML5 originally specified an "outline algorithm" in which heading levels would be computed automatically from <section> nesting, so you could write <h1> everywhere and let the nesting decide. No browser ever implemented it, and it was removed from the specification. Advice based on it still circulates. Write the real levels.
3. The text-level elements, all of them, and which pairs actually differ
Inside a paragraph you mark up individual words. There are more elements for this than most people use, and several pairs look interchangeable and are not.
<strong> versus <b>. <strong> means importance — this matters, and a screen reader may change tone. <b> means "draw attention to this without implying extra importance": a product name in a review, a keyword in an abstract. Both render bold by default. If you would say it louder out loud, it is <strong>.
<em> versus <i>. <em> means stress emphasis, the kind that changes a sentence's meaning — "I never said she took it". <i> means a stretch of text set apart in a conventional way without emphasis: a taxonomic name, a technical term on first use, a phrase in another language, a ship's name. Both render italic.
<del> and <ins> versus <s>. <del> and <ins> are edits to the document — this was removed, this was added — and both accept datetime and cite attributes. <s> means something is no longer accurate or relevant, which is exactly the case for a struck-through original price. Use <s> for the old price, <del> for a change tracked in a document.
<u> is the one to avoid almost always, because an underline on the web means a link. Its defined meaning is a non-textual annotation, such as marking a misspelling — genuinely rare.
The rest, each with the case it is actually for:
| Element | Meaning | Where it earns its place |
|---|---|---|
<mark> | Highlighted for the reader's current purpose | Search-result hit terms |
<small> | Side comments, legal small print | Disclaimers, attributions |
<cite> | The title of a work | Book, film, paper titles |
<q> | Inline quotation | Browser adds the quote marks |
<blockquote> | Block quotation, takes cite | Pull quotes |
<abbr> | Abbreviation, with title for expansion | First use of an acronym |
<dfn> | The defining instance of a term | Where you define it |
<code> | Computer code | Inline snippets |
<kbd> | Keys the user presses | Ctrl + S |
<samp> | Output from a program | Error text quoted in prose |
<var> | A variable or placeholder | Maths and API docs |
<sub> <sup> | Subscript, superscript | H₂O, x², footnote markers |
<time> | A machine-readable date | datetime="2026-08-02" |
<bdi> <bdo> | Text-direction isolation and override | User names in mixed-direction text |
<wbr> | A permitted line-break point | Long URLs in narrow columns |
<span> | No meaning at all | Styling or scripting hook only |
<time> is quietly the most useful of these in real applications. Displaying "3 days ago" is friendly for humans and useless to machines; <time datetime="2026-07-30T14:22:00Z">3 days ago</time> is both.
4. Forms, where the wrong element costs you real behaviour
Forms are where choosing the wrong element costs real functionality rather than just tidiness, because the browser does a great deal of work for you when the markup is right.
html
<form action="/checkout/address" method="post"> <!-- (1) -->
<label for="postcode">Postcode</label> <!-- (2) -->
<input
id="postcode"
name="postcode" <!-- (3) -->
type="text"
inputmode="text" <!-- (4) -->
autocomplete="postal-code" <!-- (5) -->
required <!-- (6) -->
pattern="[A-Za-z0-9 ]{5,8}"
aria-describedby="postcode-hint" <!-- (7) -->
/>
<p id="postcode-hint">For example, SW1A 1AA</p>
<button type="submit">Continue</button> <!-- (8) -->
</form>Line (1) gives the form a real destination and method. A form with these attributes submits without any JavaScript, which is the behaviour you want to keep working if a script fails to load.
Line (2) associates a label with a control through matching for and id. This does three things at once: the accessibility tree gets a name for the input, clicking the label focuses the input, and the tap target on a phone becomes the label plus the box rather than the box alone. Wrapping the input inside the <label> works equally well and needs no id.
Line (3) is the one people forget when they are used to frameworks: name is what appears in the submitted data. No name, no value in the request body, regardless of what is on screen.
Line (4) inputmode picks the on-screen keyboard on a phone without changing validation. Use inputmode="numeric" for a card number rather than type="number", because type="number" brings spinner arrows, allows exponent notation, and strips leading zeros — all wrong for a card or a postcode.
Line (5) autocomplete with a standard token lets the browser and the password manager fill the field correctly. The token list is standardised (email, given-name, postal-code, cc-number, one-time-code), and using it is the difference between a checkout that fills in one tap and one the user types out.
Line (6) required plus pattern gives native client-side validation, styleable through :invalid and :user-invalid. This is a convenience for the user and never a security control — Chapter 9.9.3 covers why every rule must be re-checked on the server, and Chapter 8.1 covers the mindset.
Line (7) aria-describedby attaches the hint to the input so a screen reader announces it after the label, instead of leaving it as loose text the user may never reach.
Line (8) sets type="submit" explicitly. A <button> inside a form defaults to type="submit", which is the cause of the classic bug where clicking a "Show password" button reloads the page. Any button that is not submitting needs type="button".
5. Images, and what alt is really for
html
<figure>
<img src="/img/sales-q3.png"
alt="Quarterly sales, rising from £1.2m in Q1 to £2.8m in Q3"
width="960" height="540"
loading="lazy" decoding="async">
<figcaption>Sales by quarter, 2026.</figcaption>
</figure>alt is a replacement, not a description. The test is: if the image failed to load, would this text do the same job in the sentence? A chart's alt should state the trend, because the trend is the reason the chart is on the page. "Chart" or "sales-q3.png" fails the test.
A decorative image takes alt="", empty and present. That tells assistive technology to skip it. Omitting the attribute entirely is different and worse: with no alt at all, screen readers commonly fall back to announcing the filename, so the user hears "img slash hero underscore banner underscore v2 dot p n g".
width and height are not deprecated and you should set them. The browser uses the ratio to reserve space before the image arrives, which prevents content jumping down the page as images load. That jump is measured directly by one of the Core Web Vitals in Chapter 6.7. CSS can still resize the image freely; the attributes only supply the aspect ratio.
loading="lazy" on below-the-fold images defers the download until they approach the viewport. Never put it on the largest image at the top of the page — you would be delaying the very thing the user is waiting for.
6. The order of things in <head>
Element order inside <head> is not stylistic. Two positions genuinely matter and the rest follows a sensible convention.
html
<head>
<meta charset="utf-8"> <!-- (1) FIRST -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <!-- (2) -->
<title>Checkout — Address</title> <!-- (3) -->
<link rel="preconnect" href="https://cdn.example.com"> <!-- (4) -->
<link rel="stylesheet" href="/app.css"> <!-- (5) -->
<script src="/app.js" defer></script> <!-- (6) -->
</head>Line (1) must come first, and the reason is precise: the browser reads the first 1024 bytes looking for the character encoding. Until it knows the encoding it cannot decode the bytes into characters at all, so it guesses. If the declaration turns up after 1024 bytes and the guess was wrong, the parser throws away everything it has done and starts again — and in the meantime, the encoding guess is a genuine security issue, because a page misinterpreted as a different encoding can turn attacker-supplied text into markup. Chapter 1.4 covers UTF-8 itself.
Line (2) is what makes a page usable on a phone. Without it, mobile browsers assume a desktop-width page and shrink it to fit, so your careful responsive CSS never activates because the reported viewport is 980 pixels wide on a 390-pixel screen.
Line (3) is the accessible name of the whole document and the first thing a screen reader announces on navigation. Put the page-specific part first — "Checkout — Address" beats "Example Store — Checkout — Address", because the shared prefix is announced every single time.
Line (4) preconnect opens the DNS, TCP and TLS connection to another origin early, so the eventual request skips all three (Chapters 5.4 and 5.7). Use it for a handful of origins you know you will hit; every open connection costs something on both ends.
Line (5) is the render-blocking stylesheet from Chapter 6.1.2, deliberately high so the download starts as early as possible.
Line (6) uses defer so it downloads in parallel with parsing and runs after the DOM is complete, in document order. Chapter 6.3.2 works through the alternatives.
What the interviewer will push on
"Why can't a <div> go inside a <p>?" <p> has a phrasing-content model, a <div> is flow content, and the parser's specified recovery is to close the paragraph. Then volunteer the empty <p> that appears from the orphaned closing tag — that detail is the difference between having read the rule and having debugged it.
"What does using a <button> instead of a clickable <div> actually buy?" A role and a name in the accessibility tree, keyboard focus, Enter/Space activation, and form submission. Four behaviours you otherwise reimplement. The weak answer is "it is more semantic", which restates the question.
"When would you use ARIA?" Only when no native element expresses what you are building — a tab set, a combobox, a live region. Quote the first rule of ARIA: do not use ARIA. Then name the sharper trap: incorrect ARIA is worse than none, because a wrong role overrides the correct native one and actively misleads the user.
"Difference between <section> and <div>?" <section> is a thematic region and should have a heading; <div> has no meaning and is the correct choice for pure grouping. A <section> with no heading adds a nameless landmark, which is noise.
"Why put <meta charset> first?" The browser scans the first 1024 bytes for it; a later declaration can force a re-parse, and a wrong guess is a security problem as well as a display one.
"What is wrong with a missing alt versus an empty one?" Empty and present means decorative, skip it. Missing means the screen reader may read the filename aloud. They are opposite outcomes from what looks like the same omission.
One thing to volunteer: mention that <tr> is silently wrapped in an implied <tbody>, so table > tr never matches in CSS. It is a small thing, but it is the kind of parser behaviour that only someone who has actually debugged a table knows, and it makes the content-model point concrete.
Recall
- Every element has a content model saying which content categories it accepts. Violating one does not error — the parser closes the open element and moves on, which is why your
<div>became a sibling of the<p>and an empty<p>appeared. - The traps: block elements in
<p>,<a>in<a>,<form>in<form>(inner dropped), non-<li>children of<ul>(moved out), and<tr>wrapped in an implied<tbody>sotable > trmatches nothing. - The browser builds an accessibility tree beside the DOM, carrying role, name and state. Your element choice fills it in. A
<div>contributes nothing; a<button>contributes a role, a name, focus, key activation and form submission. - Landmarks divide the page:
<header>,<nav>, one<main>holding only this page's unique content,<article>,<section>(only with a heading),<aside>,<footer>.<div>for pure grouping is correct, not a failure. - Headings set structure, not size. Do not skip levels downward. The HTML5 outline algorithm was never implemented and was removed — write real levels.
- The pairs that differ:
<strong>importance vs<b>attention;<em>stress vs<i>conventional set-apart;<del>/<ins>document edits vs<s>no longer accurate. - Forms:
nameis what gets submitted,<label for>supplies the accessible name and a bigger tap target,autocompletetokens drive password managers,inputmodepicks the keyboard withouttype="number"'s side effects, and<button>defaults totype="submit". altis a replacement, not a description.alt=""means decorative; missingaltmay read the filename aloud. Setwidth/heightto reserve space and stop layout shift.<meta charset>first — the browser scans 1024 bytes for it, and a wrong guess causes a re-parse and a security problem.
Self-test: What exactly does the parser do when it meets <div> inside <p>? · Name three behaviours a <button> gives you that a clickable <div> does not · When is <section> the wrong choice? · Why is a missing alt worse than an empty one? · Why must <meta charset> appear in the first 1024 bytes?
Next: 6.2.2 turns to the other input to the style step — how the browser decides which of five competing rules wins when they all target the same element, and why the answer has almost nothing to do with the order you wrote them in.