Skip to main content

Semantic HTML

Semantic HTML means using elements that describe their meaning rather than their appearance. A <nav> tells browsers, screen readers, and search engines "this is navigation" -- a <div class="nav"> tells them nothing.

Why it matters

BenefitExplanation
AccessibilityScreen readers use semantic elements to build a navigable page outline. A <main> element lets users skip to content; headings create a table of contents.
SEOSearch engines use semantic structure to understand page content. <article>, <h1>--<h6>, and <time> all provide ranking signals.
MaintainabilitySemantic markup is self-documenting. Reading <aside> is faster than deciphering <div class="sidebar-content-wrapper">.
Default behaviourMany semantic elements come with built-in functionality -- <details> is collapsible, <dialog> manages focus, <form> handles submission. No JavaScript required.
InteroperabilityRSS readers, read-mode browsers, and AI tools extract content more reliably from semantic markup.

Document structure

These elements define the high-level regions of a page.

The introductory content or navigational aids for a page or section. Typically contains a logo, title, and navigation.

<header>
<a href="/" aria-label="Home">
<img src="/logo.svg" alt="Company name" width="120" height="40">
</a>
<nav aria-label="Main">
<ul>
<li><a href="/products">Products</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>

A page can have multiple <header> elements -- one for the page and one inside each <article> or <section>.

A section of navigation links. Use aria-label to distinguish multiple <nav> elements on the same page.

<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/docs">Docs</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>

<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/docs">Docs</a></li>
<li aria-current="page">Semantic HTML</li>
</ol>
</nav>

Not every group of links needs a <nav> -- use it for major navigation blocks (main menu, breadcrumbs, table of contents, pagination).

main

The dominant content of the page. There must be only one <main> per page, and it must not be nested inside <header>, <nav>, <footer>, <article>, or <aside>.

<body>
<header><!-- site header --></header>
<main>
<h1>Semantic HTML Reference</h1>
<p>Content goes here.</p>
</main>
<footer><!-- site footer --></footer>
</body>

Screen readers offer a "skip to main content" shortcut that targets this element.

Closing content for a page or section -- copyright notices, contact info, related links.

<footer>
<p>&copy; 2025 Luca Nerlich. All rights reserved.</p>
<nav aria-label="Footer">
<a href="/privacy">Privacy</a>
<a href="/imprint">Imprint</a>
</nav>
</footer>

Like <header>, a page can have multiple footers (one per <article> or <section>).

section

A thematic grouping of content, typically with a heading. Use it when content forms a distinct section of a document.

<section aria-labelledby="features-heading">
<h2 id="features-heading">Features</h2>
<p>Our platform provides...</p>
</section>

<section aria-labelledby="pricing-heading">
<h2 id="pricing-heading">Pricing</h2>
<p>Choose the plan that fits...</p>
</section>

<section> vs <div>: If the grouping has a heading and represents a standalone concept, use <section>. If you only need a wrapper for styling, use <div>.

article

Self-contained content that makes sense on its own -- a blog post, news story, forum post, product card, or comment.

<article>
<header>
<h2>Understanding Semantic HTML</h2>
<time datetime="2025-03-15">15 March 2025</time>
</header>
<p>Semantic HTML is the practice of using...</p>
<footer>
<p>Written by <a href="/authors/luca">Luca</a></p>
</footer>
</article>

Articles can be nested (e.g. an article with nested comment articles).

aside

Content tangentially related to the surrounding content -- sidebars, pull quotes, advertising, related links.

<main>
<article>
<h1>Building a REST API</h1>
<p>Start by defining your resources...</p>

<aside>
<h2>Related reading</h2>
<ul>
<li><a href="/content-modeling">Content Modeling</a></li>
<li><a href="/web-performance">Web Performance</a></li>
</ul>
</aside>
</article>
</main>

Full page layout

Combining the structural elements:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title</title>
</head>
<body>
<header>
<nav aria-label="Main"><!-- main navigation --></nav>
</header>

<main>
<article>
<header>
<h1>Article Title</h1>
<time datetime="2025-03-15">15 March 2025</time>
</header>

<section>
<h2>First Section</h2>
<p>Content...</p>
</section>

<section>
<h2>Second Section</h2>
<p>Content...</p>
</section>

<aside>
<h2>Related</h2>
<ul><!-- related links --></ul>
</aside>

<footer>
<p>Author info, tags</p>
</footer>
</article>
</main>

<aside>
<!-- site-wide sidebar -->
</aside>

<footer>
<p>&copy; 2025</p>
<nav aria-label="Footer"><!-- footer links --></nav>
</footer>
</body>
</html>

Headings

Headings (<h1> through <h6>) create an outline that screen readers and search engines use to understand content hierarchy.

<h1>Page Title</h1>              <!-- one per page -->

<h2>Major Section</h2>
<h3>Subsection</h3>
<h3>Subsection</h3>

<h2>Another Major Section</h2>
<h3>Subsection</h3>
<h4>Sub-subsection</h4>

Rules

  • One <h1> per page -- it represents the page title
  • Do not skip levels -- go from <h2> to <h3>, not <h2> to <h4>
  • Use headings for structure, not styling -- if you need larger text, use CSS
  • Every <section> should have a heading -- it labels the section for assistive technology

Text content

Paragraphs and line breaks

<!-- Paragraph -- the default block of text -->
<p>This is a paragraph of text.</p>

<!-- Line break -- only for content where breaks matter (poems, addresses) -->
<address>
123 Main Street<br>
Springfield, IL 62701
</address>

Do not use <br> for spacing -- use CSS margin or padding instead.

Blockquote

For extended quotations from another source:

<blockquote cite="https://www.w3.org/WAI/">
<p>The power of the Web is in its universality. Access by everyone
regardless of disability is an essential aspect.</p>
<footer>-- <cite>Tim Berners-Lee</cite></footer>
</blockquote>
Rendered:

The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect.

-- Tim Berners-Lee

Figure and figcaption

Self-contained content with an optional caption -- images, diagrams, code listings, tables.

<figure>
<img src="/chart.png" alt="Monthly revenue growth from January to June"
width="800" height="400">
<figcaption>Fig. 1: Revenue grew 23% from Q1 to Q2.</figcaption>
</figure>

<!-- Also works for code -->
<figure>
<pre><code>const greeting = "Hello, world!";</code></pre>
<figcaption>A simple JavaScript variable declaration.</figcaption>
</figure>
Rendered:
const greeting = "Hello, world!";
A simple JavaScript variable declaration.

Pre and code

<pre> preserves whitespace and line breaks. <code> marks inline or block code.

<!-- Inline code -->
<p>Run <code>npm install</code> to install dependencies.</p>

<!-- Block code -->
<pre><code>function greet(name) {
return `Hello, ${name}!`;
}</code></pre>

Horizontal rule

A thematic break between sections (not just a visual line):

<section>
<h2>Chapter 1</h2>
<p>Content...</p>
</section>

<hr>

<section>
<h2>Chapter 2</h2>
<p>Content...</p>
</section>
Rendered:

Inline text semantics

These elements give meaning to words and phrases within a block of text.

ElementMeaningExample
<strong>Strong importance<strong>Warning:</strong> this will delete all data.
<em>Stress emphasisYou <em>must</em> restart the server.
<mark>Highlighted / relevant textSearch results for <mark>semantic</mark> HTML.
<del>Deleted / removed textPrice: <del>$49</del> <ins>$29</ins>
<ins>Inserted / added textUsed alongside <del> for edits
<abbr>Abbreviation<abbr title="Hypertext Markup Language">HTML</abbr>
<cite>Title of a work<cite>The Pragmatic Programmer</cite>
<time>Machine-readable date/time<time datetime="2025-03-15">15 March 2025</time>
<kbd>Keyboard inputPress <kbd>Ctrl</kbd>+<kbd>S</kbd> to save.
<samp>Sample outputThe console prints <samp>Hello, world!</samp>
<var>VariableLet <var>x</var> be the number of users.
<small>Side comment, fine print<small>Terms and conditions apply.</small>
<sub>SubscriptH<sub>2</sub>O
<sup>SuperscriptE = mc<sup>2</sup>
<q>Inline quotationShe said <q>it works on my machine</q>.
<dfn>Definition termA <dfn>CDN</dfn> is a content delivery network.
<data>Machine-readable value<data value="42">forty-two</data>
Rendered examples:

Warning: this will delete all data.

You must restart the server.

Search results for semantic HTML.

Price: $49 $29

HTML is the standard markup language.

Read The Pragmatic Programmer by Hunt and Thomas.

Published on .

Press Ctrl+S to save.

The console prints Hello, world!

Let x be the number of users.

Terms and conditions apply.

H2O    E = mc2

She said it works on my machine.

A CDN is a content delivery network.

strong vs b, em vs i

SemanticVisual equivalentWhen to use semantic
<strong><b>Content is important -- screen readers change tone
<em><i>Stress emphasis changes the meaning of the sentence

Use <b> and <i> only for stylistic purposes without semantic meaning (e.g. a product name in bold, a foreign phrase in italics).

time

The datetime attribute makes dates machine-readable for search engines, calendar apps, and translation tools:

<time datetime="2025-03-15">15 March 2025</time>
<time datetime="14:30">2:30 PM</time>
<time datetime="2025-03-15T14:30:00+01:00">15 March 2025, 2:30 PM CET</time>
<time datetime="P3D">3 days</time>

Lists

Unordered list

Items where order does not matter:

<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>

Ordered list

Items where order matters -- steps, rankings, instructions:

<ol>
<li>Clone the repository</li>
<li>Install dependencies</li>
<li>Start the dev server</li>
</ol>

<!-- Start from a different number -->
<ol start="5">
<li>Fifth item</li>
<li>Sixth item</li>
</ol>

<!-- Reversed -->
<ol reversed>
<li>Bronze</li>
<li>Silver</li>
<li>Gold</li>
</ol>

Description list

Key-value pairs -- glossaries, metadata, FAQs:

<dl>
<dt>HTML</dt>
<dd>HyperText Markup Language -- the standard language for web pages.</dd>

<dt>CSS</dt>
<dd>Cascading Style Sheets -- controls the visual presentation of HTML.</dd>

<dt>JavaScript</dt>
<dd>A programming language for interactive web content.</dd>
</dl>
Rendered:
HTML
HyperText Markup Language -- the standard language for web pages.
CSS
Cascading Style Sheets -- controls the visual presentation of HTML.
JavaScript
A programming language for interactive web content.

A <dt> can have multiple <dd> values, and multiple <dt> terms can share a <dd>.

Tables

Tables are for tabular data -- never for layout.

<table>
<caption>Quarterly Revenue (in thousands)</caption>
<thead>
<tr>
<th scope="col">Quarter</th>
<th scope="col">Revenue</th>
<th scope="col">Growth</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Q1 2025</th>
<td>$120</td>
<td>+5%</td>
</tr>
<tr>
<th scope="row">Q2 2025</th>
<td>$148</td>
<td>+23%</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td>$268</td>
<td>--</td>
</tr>
</tfoot>
</table>
Rendered:
Quarterly Revenue (in thousands)
QuarterRevenueGrowth
Q1 2025$120+5%
Q2 2025$148+23%
Total$268--

Table elements

ElementPurpose
<table>Table container
<caption>Visible title / description of the table
<thead>Header row group
<tbody>Body row group
<tfoot>Footer row group (summaries, totals)
<tr>Table row
<th>Header cell -- use scope="col" or scope="row" for accessibility
<td>Data cell
<colgroup> / <col>Column styling (width, background)

Accessibility tips

  • Always use <caption> -- it tells screen readers what the table contains
  • Use scope="col" on column headers and scope="row" on row headers
  • For complex tables with merged cells, use headers attribute to associate data cells with their headers

Forms

Forms are where semantic HTML provides the most built-in functionality for free.

Basic form structure

<form action="/subscribe" method="post">
<fieldset>
<legend>Newsletter Signup</legend>

<div>
<label for="email">Email address</label>
<input type="email" id="email" name="email"
required autocomplete="email"
placeholder="you@example.com">
</div>

<div>
<label for="name">Full name</label>
<input type="text" id="name" name="name"
required autocomplete="name">
</div>

<button type="submit">Subscribe</button>
</fieldset>
</form>
Rendered:
Newsletter Signup

fieldset and legend

Group related fields and give the group a label:

<fieldset>
<legend>Shipping address</legend>
<!-- address fields -->
</fieldset>

<fieldset>
<legend>Billing address</legend>
<!-- address fields -->
</fieldset>

Screen readers announce the <legend> when entering a fieldset, giving users context.

label

Every input needs a label. Use for to associate them:

<!-- Explicit association (preferred) -->
<label for="username">Username</label>
<input type="text" id="username" name="username">

<!-- Implicit association (wrapping) -->
<label>
Username
<input type="text" name="username">
</label>

Never use placeholder as a replacement for <label> -- placeholders disappear on focus and are not reliably read by screen readers.

Input types

Using the correct type gives you free validation, the right mobile keyboard, and autofill support:

TypePurposeMobile keyboard
textGeneric textStandard
emailEmail address@ key, .com shortcut
telPhone numberNumeric dialpad
urlURL.com, / keys
numberNumeric valueNumber pad
passwordPasswordObscured input
searchSearch querySearch/enter key
dateDate pickerNative date picker
timeTime pickerNative time picker
datetime-localDate and timeCombined picker
colorColour pickerNative colour picker
rangeSliderSlider control
fileFile uploadFile browser
checkboxBoolean toggleCheckmark
radioOne-of-many selectionRadio button
hiddenHidden value (not for secrets)Not visible
Rendered -- native input types:

select

Dropdown selection:

<label for="country">Country</label>
<select id="country" name="country">
<option value="">-- Select --</option>
<optgroup label="Europe">
<option value="de">Germany</option>
<option value="fr">France</option>
</optgroup>
<optgroup label="North America">
<option value="us">United States</option>
<option value="ca">Canada</option>
</optgroup>
</select>
Rendered:

textarea

Multi-line text input:

<label for="message">Message</label>
<textarea id="message" name="message" rows="5" cols="40"
maxlength="1000" placeholder="Your message..."></textarea>

output

Displays the result of a calculation or user action:

<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
<input type="number" id="a" name="a" value="0"> +
<input type="number" id="b" name="b" value="0"> =
<output name="result" for="a b">0</output>
</form>

progress and meter

<!-- Progress: task completion (uploading, loading) -->
<label for="upload">Upload progress:</label>
<progress id="upload" value="70" max="100">70%</progress>

<!-- Meter: scalar measurement within a known range -->
<label for="disk">Disk usage:</label>
<meter id="disk" value="0.7" min="0" max="1"
low="0.3" high="0.8" optimum="0.1">70%</meter>
ElementUse forSemantics
<progress>Task completion (upload, loading bar)Represents progress toward a goal
<meter>Scalar measurement (disk usage, score, rating)Represents a value within a known range
Rendered:
70%
70%

Interactive elements

details and summary

A native collapsible disclosure widget -- no JavaScript required:

<details>
<summary>System requirements</summary>
<ul>
<li>Node.js 18+</li>
<li>npm 9+</li>
<li>Git 2.30+</li>
</ul>
</details>

<!-- Open by default -->
<details open>
<summary>Quick start</summary>
<pre><code>npm create next-app@latest</code></pre>
</details>
Rendered (click to toggle):
System requirements
  • Node.js 18+
  • npm 9+
  • Git 2.30+
Quick start
npm create next-app@latest

dialog

A modal or non-modal dialog box with built-in focus management:

<dialog id="confirm-dialog">
<h2>Confirm deletion</h2>
<p>Are you sure you want to delete this item?</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>

<button onclick="document.getElementById('confirm-dialog').showModal()">
Delete item
</button>

When opened with .showModal():

  • A backdrop overlay appears automatically
  • Focus is trapped inside the dialog
  • Pressing Esc closes it
  • The <form method="dialog"> closes the dialog on submit

Media

picture

Serve different image formats and sizes based on browser support and viewport:

<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="Description of the image"
width="800" height="600" loading="lazy">
</picture>

See the Web Performance -- Images section for responsive image patterns with srcset and sizes.

video

<video controls width="720" height="480" preload="metadata"
poster="thumbnail.jpg">
<source src="video.mp4" type="video/mp4">
<source src="video.webm" type="video/webm">
<track kind="captions" src="captions-en.vtt"
srclang="en" label="English" default>
<p>Your browser does not support HTML video.
<a href="video.mp4">Download the video</a>.</p>
</video>
  • poster -- thumbnail shown before playback
  • preload="metadata" -- loads duration and dimensions without downloading the full video
  • <track> -- captions, subtitles, or descriptions for accessibility

audio

<audio controls preload="metadata">
<source src="podcast.mp3" type="audio/mpeg">
<source src="podcast.ogg" type="audio/ogg">
<p>Your browser does not support HTML audio.
<a href="podcast.mp3">Download the episode</a>.</p>
</audio>

Less common but useful elements

address

Contact information for the nearest <article> or <body> ancestor:

<address>
<a href="mailto:luca.nerlich@gmail.com">luca.nerlich@gmail.com</a><br>
<a href="https://github.com/LucaNerlich">GitHub</a>
</address>
Rendered:
luca.nerlich@gmail.com
GitHub

Not for arbitrary postal addresses -- use <p> for those.

template

A fragment of HTML that is not rendered but can be cloned by JavaScript:

<template id="card-template">
<article class="card">
<h3></h3>
<p></p>
</article>
</template>

<script>
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('h3').textContent = 'Card Title';
clone.querySelector('p').textContent = 'Card description.';
document.body.appendChild(clone);
</script>

slot (Web Components)

A placeholder inside a Web Component's shadow DOM that receives content from the light DOM. See the JavaScript: Web Components section for details.

map and area

Image maps -- clickable regions on an image:

<img src="floor-plan.png" alt="Office floor plan"
usemap="#office" width="600" height="400">
<map name="office">
<area shape="rect" coords="0,0,200,200"
href="/rooms/meeting" alt="Meeting room">
<area shape="circle" coords="400,300,50"
href="/rooms/kitchen" alt="Kitchen">
</map>

Rarely used today -- CSS and SVG overlays are more flexible.

Anti-patterns

Div soup

<!-- Bad: divs everywhere, no meaning -->
<div class="header">
<div class="nav">
<div class="nav-item"><a href="/">Home</a></div>
</div>
</div>
<div class="main">
<div class="article">
<div class="title">My Post</div>
</div>
</div>

<!-- Good: semantic elements -->
<header>
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>My Post</h1>
</article>
</main>

Other common mistakes

MistakeProblemFix
Using <div> for everythingNo semantic meaning for assistive tech or SEOUse the right element for the content
Skipping heading levels (<h1> then <h4>)Broken document outlineGo <h1>, <h2>, <h3> in order
Using <table> for page layoutScreen readers announce it as data, confusing usersUse CSS Grid or Flexbox
Using <br> for spacingFragile, not the element's purposeUse CSS margin/padding
Placeholder instead of labelDisappears on focus, poor accessibilityAlways use <label>
Click handlers on <div> or <span>Not keyboard accessible, no focus, no roleUse <button> or <a>
Using <a> without hrefNot focusable or keyboard navigableAdd href, or use <button> for actions
<b> and <i> for emphasisNo semantic meaningUse <strong> and <em>

Quick reference

A cheat sheet for choosing the right element:

You want to...Use
Wrap the page header<header>
Create a navigation menu<nav>
Mark the main content area<main>
Wrap the page footer<footer>
Create a sidebar or tangential content<aside>
Group content with a heading<section>
Mark standalone, redistributable content<article>
Show a date or time<time datetime="...">
Display an image with a caption<figure> + <figcaption>
Quote a block of text<blockquote>
Define a term<dfn>
Show an abbreviation<abbr title="...">
Group related form fields<fieldset> + <legend>
Create a collapsible section<details> + <summary>
Show a modal dialog<dialog>
Display key-value pairs<dl> + <dt> + <dd>
Show task progress<progress>
Show a measurement in a range<meter>
Emphasize importance<strong>
Add stress emphasis<em>
Highlight a search term<mark>

Summary

Semantic HTML is not extra work -- it is the default way to write HTML. Every time you reach for a <div> or <span>, ask: "Is there an element that already describes what this content is?" If yes, use it. You get accessibility, SEO, built-in behaviour, and self-documenting code for free.