../

Semantic elements

Choosing the element that says what the content is: content categories and nesting rules, landmarks and headings, text-level and grouping elements, tables, links, global attributes and the obsolete tags to stop using. The document skeleton and <head> are in Document & head, form controls in Forms & inputs, media in Media & embeds, and ARIA and testing in Accessibility.

Why semantics

ConsumerWhat the right element gives it
Accessibility treeeach element maps to a role (<nav> → navigation, <button> → button) with a name and state; screen-reader users jump by landmark, heading, list, table and link
Default behavior<button> is focusable and fires on Enter and Space; <a href> opens in a new tab on middle-click; <details> toggles; <label> focuses its input. A <div onclick> has none of this
Search enginesheadings, <title>, link text, <time>, tables and lists help crawlers understand structure; structured data does the rest
Reader mode & translationSafari Reader, Firefox Reader View and translators pick the <article>/<main> body and skip <nav>/<aside>; translate="no" protects brand names
Default styleslists get markers, <em> italics, <code> monospace; they survive when CSS fails to load
Future you<section class="pricing"> reads better than <div class="pricing"> five levels deep

Rule of thumb: pick the element for meaning, then style it. Reach for <div> and <span> only when no element fits, and add ARIA only when HTML has no native equivalent (the first rule of ARIA use (opens in a new tab)).

Content categories & content model

Every element belongs to one or more content categories, and every element's content model says which categories may go inside it.

CategoryMembers (main ones)Typical rule
Metadatabase, link, meta, noscript, script, style, template, titlegoes in head (some also allowed in body)
Flowalmost everything allowed in bodyblock-level containers accept flow
Sectioningarticle, aside, nav, sectiondefine the scope of header, footer and headings
Headingh1–h6, hgroup
Phrasingtext, a, abbr, b, br, button, code, em, img, input, label, mark, q, span, strong, time, wbr…what goes inside paragraphs and headings
Embeddedaudio, canvas, embed, iframe, img, math, object, picture, svg, video
Interactivea[href], button, details, embed, iframe, input (not hidden), label, select, textarea, audio[controls], video[controls]may not nest inside each other
Form-associatedbutton, fieldset, input, object, output, select, textarea, img, form-associated custom elementscan belong to a form
ElementMay containMay not contain
p, h1–h6phrasing contentdiv, lists, table, other p (the parser closes the p)
atransparent: whatever its parent allows, so a block link around a card is validinteractive content, another a, anything with tabindex
buttonphrasing contentinteractive content, anything with tabindex
labelphrasing contentother labelable controls besides its own, another label
ul, ol, menuli (plus script/template)bare text, div
dldt/dd groups, or divs each wrapping one groupbare text
tablecaption, colgroup, thead, tbody, tfoot, tra tr directly under table gets an implied tbody
formflow contentanother form
header, footerflow contentheader, footer, main
addressflow contentheadings, sectioning content, header, footer, address
mainflow contentmay only sit inside html, body, div, an unnamed form, or a custom element; not in article, aside, header, footer, nav

The parser repairs invalid nesting instead of failing, and the repair often isn't what you meant. A <p>'s end tag is implied as soon as a block element such as div, ul, table, section, h1–h6, pre, blockquote, figure, form or another p starts, so the div below is not inside the paragraph, "more text" ends up outside any paragraph, and the stray </p> creates a new, empty paragraph:

p-autoclose.html
<style>
  p { outline: 2px dashed var(--graph-1);
    padding: 4px; margin: 6px 0; min-height: 8px; }
  div { outline: 2px solid var(--graph-0); padding: 4px; }
</style>
<p>Paragraph text
  <div>A div written "inside" the p</div>
  more text
</p>
Result

Other repairs to know: a <table> gets an implied <tbody> (so table > tr selectors never match), text directly inside <table> is moved in front of the table ("foster parenting"), and an <a> opened inside another <a> closes the first.

Sectioning & landmarks

Landmarks are the regions screen-reader users list and jump between. HTML elements create them without any ARIA.

ElementImplicit roleLandmark?Use for
headerbanner at page level; generic inside article, aside, main, nav, sectiononly at page levelsite header: logo, site nav, search
navnavigationyesmajor blocks of navigation links; label each one when there are several (aria-label="Primary")
mainmainyesthe page's unique content; exactly one visible per page
asidecomplementary; generic if unnamed and nested in sectioning contenttop-level, or when namedcontent tangential to its parent: sidebar, pull quote, related links
footercontentinfo at page level; generic inside article, aside, main, nav, sectiononly at page levelcopyright, contact, legal links
sectionregion if it has an accessible name, otherwise genericonly when nameda thematic group with its own heading
articlearticleno (but listed by some screen readers)self-contained, independently distributable: a post, a comment, a product card
searchsearchyeswraps a search or filter form. Baseline widely available since April 2026
formformonly when named (aria-label, aria-labelledby)
addressgroupnocontact details for the nearest article or the page; not for arbitrary postal addresses
div, spangenericnostyling hooks when nothing else fits
  • section vs article: would it make sense on its own in a feed or another site? article. Otherwise, does it have a heading? section. Neither? div.
  • Name a section by pointing at its heading: <section aria-labelledby="faq-h"> with <h2 id="faq-h">. That turns it into a region landmark; do it only for regions worth jumping to.
  • Every piece of content should sit inside some landmark, so screen-reader users navigating by region don't miss it.
  • One banner, one main, one contentinfo per page. Several navs are fine if each is named.

Headings & the outline

The HTML5 document outline algorithm, in which <h1> inside nested <section>s would be demoted automatically, was never implemented by any browser or screen reader. WHATWG removed it from the standard in July 2022 (pull request #7829 by Steve Faulkner) and replaced it with an outline built from heading levels alone. The UA styles that shrank an <h1> inside section/article/aside/nav were removed from the spec in May 2025 and from Chrome 140, Firefox 140 and Safari 26.2, so a nested <h1> now renders full size. MDN notes that nested <h1>s are now non-conforming.

RuleDetail
One h1 per pagemultiple h1s are technically allowed if not nested, but MDN and most audits recommend one, matching the <title>
No skipped levelsthe spec now says each heading must be at most one level deeper than the one before (h2 → h4 is non-conforming); going back up any number is fine
Level = rank, not sizestyle with classes (.h-small); never pick h4 because it looks right
Headings are for sectionsnot for bold text, taglines or "Follow us" boxes that head nothing
hgroupa heading plus p subtitles: <hgroup><h1>Dune</h1><p>A novel</p></hgroup>; the old "several headings in one hgroup" model is gone
headingoffsetnew spec attribute that shifts descendant heading levels (for reusable components); behind flags in Chrome and Firefox, not usable yet
<h1>HTML reference</h1>        <!-- one per page -->
  <h2>Sectioning</h2>
    <h3>header and footer</h3>
    <h3>section vs article</h3>
  <h2>Headings</h2>            <!-- back up: fine -->

Text-level semantics

ElementMeansNot for
emstress emphasis that changes the sentence's meaning ("I did pay")visual italics
ialternate voice: technical terms, foreign phrases (add lang), thoughts, ship names, taxonomic namesemphasis, icons (<i class="icon"> is a hack)
strongimportance, seriousness or urgency ("Warning:")headings, visual bold
battention without extra importance: keywords, product names, a ledeanything that matters more than its surroundings
markhighlighted for relevance in this context: search hits, a quoted passage's key phraseemphasis the author adds
smallside comments, fine print, legal text, attributionmaking text less important
sno longer accurate or relevant (an old price)document edits
del / instext removed from / added to the document; cite (URL) and datetime attributesoutdated-but-kept content (s)
uunarticulated annotation: a spelling-error squiggle, a Chinese proper-name markunderlining for style (it looks like a link)
citethe title of a work: book, film, paper, programa person's name (the WHATWG spec forbids it)
qinline quotation; the browser adds language-appropriate quote marks; cite attribute for the source URLtyping your own quotes too
abbrabbreviation; title gives the expansionrelying on title alone: expand on first use in text
dfnthe defining instance of a term (the p, dt or section around it holds the definition)every later mention
codea fragment of computer codekeyboard input
kbduser input: keys, commands to type; nest for key combosoutput
sampsample output from a programinput
vara variable in math or code
sub / suptypographic convention only: chemical formulae, footnote markers, French abbreviations such as Mllemath layout (use MathML)
timea date, time or duration; datetime holds the machine-readable valuevague spans ("soon")
dataa machine-readable value for any content (<data value="8712345">Blue mug</data>)dates (use time)
bdiisolates text of unknown direction (user names in RTL/LTR mixes)
bdooverrides direction with dirnormal bidi text
bra line break that is part of the content: addresses, poemsspacing between paragraphs
wbra line-break opportunity inside a long word or URLhyphenation (use &shy; or hyphens: auto)
spannothing; a hook for lang, class, dir
text-level.html
<p><em>Stress</em> vs <strong>importance</strong>;
  <i lang="la">in vitro</i>; <b>keyword</b>.</p>
<p><mark>match</mark> · <small>fine print</small> ·
  <s>£40</s> £30 · <del>removed</del> <ins>added</ins></p>
<p><q>Fear is the mind-killer</q> from <cite>Dune</cite>
  · <abbr title="HyperText Markup Language">HTML</abbr></p>
<p><kbd><kbd>Ctrl</kbd>+<kbd>C</kbd></kbd> ·
  <code>npm i</code> · <samp>Error 404</samp> ·
  <var>x</var><sup>2</sup> · H<sub>2</sub>O</p>
Result

time formats

Kinddatetime value
Date2026-09-26
Month2026-09
Yearless date09-26
Year2026 (four or more digits)
Week2026-W39
Time14:30, 14:30:15, 14:30:15.250
Local date and time2026-09-26T14:30 (a space instead of T is also valid)
Global date and time2026-09-26T13:30Z, 2026-09-26T14:30+01:00
Time-zone offsetZ, +01:00, -0800
DurationPT2H30M, P3DT4H, or 2h 30m
<p>Published <time datetime="2026-09-26">26 Sept</time>.
  Doors open <time datetime="19:30">7.30pm</time>;
  the talk runs <time datetime="PT45M">45 min</time>.</p>

Without datetime, the element's text must itself be one of these formats.

Grouping content

ElementUseNotes
pa paragraphphrasing only; never empty <p>s for spacing
blockquotean extended quotationcite attribute holds the source URL (not shown); the spec puts the attribution outside the blockquote, typically in a figcaption
prepreformatted text: whitespace and line breaks kepta newline right after <pre> is dropped; escape < and &; wrap code in <pre><code>
figure + figcaptionself-contained content referred to from the main text: image, chart, code listing, quote, tablefigcaption first or last child; it becomes the figure's accessible name
hra thematic break between paragraphs (scene change, topic shift)role separator; not a decorative line (use a border)
divno meaning; wrapper for styling or for dt/dd groupslast resort
mainsee landmarks
ulunordered list: order doesn't matternav menus are lists of links
olordered list: start="5", reversed, type="1 | a | A | i | I"; li value="10" jumps the counttype is meaningful (legal clauses), so it lives in HTML, not only CSS
menua toolbar-style list of commands; treated like ulnot the old context-menu element
dl / dt / ddname–value groups: glossaries, metadata, FAQs; several dt per dd or several dd per dtwrap each group in a div for styling (valid since 2017)
lists.html
<style>
  .cols { display: flex; gap: 28px; flex-wrap: wrap; }
  dt { font-weight: 600; }
  dl div { margin-block-end: 6px; }
  dd { margin-inline-start: 12px; }
</style>
<div class="cols">
  <ol reversed start="3">
    <li>Bronze</li><li>Silver</li><li>Gold</li></ol>
  <ol type="i">
    <li>Scope</li><li>Terms</li><li>Fees</li></ol>
  <dl>
    <div><dt>HTML</dt><dd>Structure</dd></div>
    <div><dt>CSS</dt><dd>Presentation</dd></div>
    <div><dt>JS</dt><dt>Wasm</dt><dd>Behavior</dd></div>
  </dl>
</div>
Result

Tables

Tables are for data with rows and columns, never for layout.

Element / attributeJob
captionthe table's title and accessible name; first child of table
thead, tbody, tfootrow groups; browsers repeat thead on each printed page; tfoot may sit after tbody; several tbodys split sections
tha header cell; bold and centered by default
scope="col" / "row"which cells a th heads; needed for row headers and anything irregular
scope="colgroup" / "rowgroup"a header spanning a group of columns or a tbody
headers="id1 id2" on tdexplicit list of header ids for complex tables where scope can't express it
colspan, rowspanmerged cells; keep them rare, they make screen-reader navigation harder
colgroup / col span="2"style whole columns; only background, border, width and visibility apply
aria-sort on thascending / descending for sortable columns
table.html
<style>
  table { border-collapse: collapse; }
  caption { text-align: start; font-weight: 600;
    padding-block-end: 6px; }
  th, td { padding: 4px 10px;
    border-block-end: 1px solid var(--chip); }
  td { text-align: end; font-variant-numeric: tabular-nums; }
  th[scope="row"] { text-align: start; }
  tfoot th, tfoot td { font-weight: 600; }
</style>
<table>
  <caption>Quarterly revenue, £k</caption>
  <thead>
    <tr><th scope="col">Region</th>
      <th scope="col">Q1</th><th scope="col">Q2</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">UK</th><td>120</td><td>135</td></tr>
    <tr><th scope="row">EU</th><td>98</td><td>110</td></tr>
  </tbody>
  <tfoot>
    <tr><th scope="row">Total</th>
      <td>218</td><td>245</td></tr>
  </tfoot>
</table>
Result
  • Right-align numbers and use tabular-nums so digits line up.
  • A wide table needs a scroll container: wrap it in <div tabindex="0" role="region" aria-labelledby="cap-id"> so keyboard users can scroll it.
  • If you are stuck with a layout table, role="presentation" removes its table semantics.

href values

FormExampleNotes
Absolutehttps://example.com/aexternal links
Root-relative/pricing/the usual choice for internal links
Relative../img/a.pngresolved against the current URL (or <base>)
Fragment#faqscrolls to id="faq"; #top or # go to the top
Text fragment/post/#:~:text=exact%20phrasescrolls to and highlights text: text=[prefix-,]start[,end][,-suffix]; Baseline 2024
mailto:mailto:hi@acme.example?subject=Hiopens the mail client
tel: / sms:tel:+442071234567international format, no spaces
javascript:avoid: use a <button>
(missing)<a>a placeholder link: not focusable, role generic

Text fragments are case-insensitive, match whole words, can repeat (#:~:text=one&text=two) and are ignored if nothing matches. Style the highlight with ::target-text.

AttributeValuesNotes
target_blank, _self, _parent, _top, a named frame_blank now implies rel="noopener" in every current browser; warn users that a new tab opens
rel="noopener"new page gets no window.opener; the default for _blank anyway
rel="noreferrer"no Referer header and implies noopener
rel="nofollow"don't endorse or crawl this link
rel="ugc"user-generated content: comments, forum posts
rel="sponsored"paid or affiliate links. Google treats nofollow, ugc and sponsored as hints and generally doesn't follow them; they combine (rel="ugc nofollow")
rel="external", "me", "author", "license", "prev"/"next"informational; me verifies profiles (Mastodon)
downloadoptional file namesame-origin, blob: and data: URLs only; cross-origin links just navigate
hreflangde, en-GBlanguage of the target (hint)
typeMIME typehint only
referrerpolicyno-referrer, origin…per-link referrer policy
pingspace-separated URLsPOSTs on click; not supported in Firefox by default

Link text must make sense out of context ("Download the 2026 report", not "click here"); screen readers list links on their own. A link goes somewhere (a URL); a button does something. If it has no URL, it's a <button>.

Global attributes

AttributeValuesNotes
idunique, no whitespacefragment target, label for, ARIA references; also creates a window global (don't rely on it)
classspace-separated tokensstyling and script hooks
hiddenhidden / until-foundhidden = display: none (removed from the accessibility tree; CSS display overrides it). until-found keeps content findable by find-in-page and fragment links and reveals it on match (beforematch event); Chromium and Firefox, partial in Safari, so not Baseline
langBCP 47: en, en-GB, zh-Hanton html and on any passage in another language
dirltr, rtl, autoauto for user-generated text of unknown direction
titletexttooltip on mouse hover only; unreachable by touch and keyboard, inconsistently announced: never put essential information here
tabindex0, -1, positive0 = focusable in order; -1 = focusable by script only; positive values break the order: never
inertbooleanthe subtree can't be focused, clicked or found, and is hidden from assistive tech; Baseline 2023
contenteditabletrue, false, plaintext-onlyplaintext-only is Baseline 2025
draggabletrue, falseenumerated, not boolean: write the value
spellchecktrue, falseturn off for codes, usernames, email addresses
translateyes, nostops machine translation of names and code; Baseline 2023
autofocusbooleanfocuses on load (or when a dialog/popover opens); one per page; can disorient screen-reader users
popoverauto, manual, hintturns the element into a popover; Baseline 2025 (hint is Chromium and Firefox only); see Interactive elements
data-*any stringcustom data: data-state="open", read as el.dataset.state
inputmodenone, text, decimal, numeric, tel, search, email, urlvirtual keyboard hint; also for contenteditable
enterkeyhintenter, done, go, next, previous, search, sendlabel of the virtual keyboard's Enter key
autocapitalizeoff, sentences, words, charactersvirtual keyboards only
autocorrecton, offBaseline since September 2026
accesskeya characterclashes with assistive-tech shortcuts; avoid
styleCSS declarationsblocked by strict CSP (style-src without 'unsafe-inline')
noncerandom tokenlets an inline script/style through CSP
slot, part, exportparts, isweb components; is (customized built-ins) is not supported in Safari

Boolean attributes are true when present, whatever the value: hidden="false" still hides. Remove the attribute to turn it off.

Character references & whitespace

WriteForWhen it's needed
&amp;&when the next characters could read as a reference (&copy in a URL query); always safe
&lt;<in text, always (or it may start a tag)
&gt;>optional; used for symmetry
&quot; / &#39;" / 'inside an attribute quoted with the same character
&nbsp;non-breaking spacekeeps 10&nbsp;kg or Fig.&nbsp;3 together
&shy;soft hyphena hyphenation point shown only at a line break
&#8212;, &#x2014;any code point, decimal or hexcharacters you can't type

With <meta charset="utf-8"> just type é, —, → and £ directly; named references for them work but add noise.

Whitespace collapsing: in normal flow, runs of spaces, tabs and newlines collapse into one space, and leading and trailing whitespace in a line is removed. Consequences:

  • Indenting markup is free, but a newline between two inline-block elements renders as a gap (use flex or grid gaps instead of fighting it).
  • To keep whitespace, use <pre> or CSS white-space: pre-wrap (pre-line keeps only newlines).
  • &nbsp; doesn't collapse; don't use runs of it for layout.

Obsolete & deprecated elements

All of these still render (browsers never break old pages), but they are non-conforming.

ObsoleteUse instead
center, font, big, tt, basefontCSS (text-align, font-*, font-family: monospace)
strikes (no longer accurate) or del (removed)
acronymabbr
marquee, blinknothing; CSS animation with prefers-reduced-motion if you must
frame, frameset, noframesiframe, or better, one page
applet, paramnothing; object with data, or embed
dirul
nobrCSS white-space: nowrap
xmp, listing, plaintextpre with escaped content
rb, rtcruby with rt (and rp fallbacks)
keygen, menuitem, isindex, bgsoundremoved; Web Crypto, a real menu, a search form, audio
a name="x"id="x" on the target element
table summary, align, bgcolor, valign, width on cellscaption, CSS
script language, type="text/javascript"omit (classic script is the default)

Common mistakes

MistakeWhy it's wrongFix
<div onclick> buttonsnot focusable, no Enter/Space, no role<button type="button">
<a href="#"> for actionswrong role; jumps to top; opens in new tab on middle-click<button>
Button or link inside a link (clickable card with a menu)invalid; unpredictable click and focusone real link, stretched with a ::after overlay; other controls positioned above it
Heading picked for sizebroken outline for screen-reader navigationright level, style with a class
<br><br> for spacingfake paragraphsseparate <p>s, CSS margins
<b>/<i> as "bold/italic" and <strong>/<em> as "semantic bold/italic"they have distinct meaningspick by meaning, see the table
section everywhereunnamed sections are just divs; named ones clutter landmark listssection only for headed thematic groups
Several unlabelled navsscreen reader lists "navigation, navigation, navigation"aria-label each
Table for layout, or data as div gridswrong semantics either wayCSS grid for layout, table for data
title for important infoinvisible on touch and to keyboard usersvisible text
placeholder or title as a labelnot a reliable accessible namelabel (Forms & inputs)
Missing lang on quoted foreign textwrong pronunciation<i lang="fr"> or <span lang="fr">
Empty links wrapping an icon onlyno accessible namevisually hidden text or aria-label

Recipes

Article page skeleton

post.html
<body>
  <a class="skip" href="#main">Skip to content</a>
  <header>
    <a href="/" aria-label="Acme home">
      <img src="/logo.svg" alt="" width="96" height="32">
    </a>
    <nav aria-label="Primary">
      <ul>
        <li><a href="/docs/">Docs</a></li>
        <li><a href="/blog/" aria-current="true">
          Blog</a></li>
      </ul>
    </nav>
    <search>
      <form action="/search/">
        <label for="q">Search</label>
        <input id="q" name="q" type="search">
      </form>
    </search>
  </header>
 
  <main id="main">
    <article>
      <h1>Faster builds</h1>
      <p>…</p>
    </article>
    <aside aria-label="Related posts">…</aside>
  </main>
 
  <footer>
    <p><small>© 2026 Acme Ltd</small></p>
  </footer>
</body>

<search> supplies the search landmark, so the form needs no role="search". The skip link is the first focusable element and targets main. aria-current="true" marks the current section in the primary nav; use "page" only on a link to the page itself.

Accessible data table

Two header rows with column groups, and row headers:

<table>
  <caption>Train times, weekdays</caption>
  <colgroup><col></colgroup>
  <colgroup span="2"></colgroup>
  <colgroup span="2"></colgroup>
  <thead>
    <tr>
      <td rowspan="2"></td>
      <th scope="colgroup" colspan="2">Morning</th>
      <th scope="colgroup" colspan="2">Evening</th>
    </tr>
    <tr>
      <th scope="col">Dep</th><th scope="col">Arr</th>
      <th scope="col">Dep</th><th scope="col">Arr</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Leeds</th>
      <td><time>07:05</time></td><td><time>09:18</time></td>
      <td><time>17:35</time></td><td><time>19:50</time></td>
    </tr>
  </tbody>
</table>

Blog post with time and figure

<article>
  <header>
    <h1>Faster builds</h1>
    <p>By <a href="/team/sam-lee/" rel="author">Sam Lee</a>
      · <time datetime="2026-09-26">26 September 2026</time>
      · updated <time datetime="2026-09-27T14:20+01:00">
        27 Sept, 14:20</time></p>
  </header>
 
  <p>We moved to <dfn>remote caching</dfn>: each task's
    output is stored by a hash of its inputs.</p>
 
  <figure>
    <img src="/img/build-times.png" width="1200"
      height="630" alt="Bar chart: builds fell from
      9 minutes in June to 2 minutes in September.">
    <figcaption>Median CI build time, 2026.</figcaption>
  </figure>
 
  <figure>
    <blockquote cite="https://acme.example/retro/">
      <p>We got an hour a day back.</p>
    </blockquote>
    <figcaption>Priya, platform team</figcaption>
  </figure>
 
  <footer>
    <p>Tags: <a href="/tags/ci/" rel="tag">CI</a></p>
  </footer>
</article>

The header and footer inside the article are not landmarks, just the article's intro and footer.

<nav aria-label="Breadcrumb" class="breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li><a href="/docs/">Docs</a></li>
    <li><a href="/docs/html/" aria-current="page">
      HTML</a></li>
  </ol>
</nav>
.breadcrumb ol {
  display: flex; flex-wrap: wrap; gap: 0.5ch;
  list-style: none; padding: 0;
}
/* separator drawn by CSS, so it isn't read out */
.breadcrumb li + li::before {
  content: "/" / "";
  margin-inline-end: 0.5ch;
}

An ordered list conveys position ("3 of 3"), aria-current="page" marks where you are, and the content: "/" / "" syntax gives the separator empty alternative text. Pair it with BreadcrumbList JSON-LD if you want breadcrumbs in search results.

References