Semantic HTML
A div is an element with no meaning. That is its purpose: it is a box for grouping things when no better element exists. The trouble is that it is also the easiest element to type, so pages get built entirely out of boxes, and then nothing about them says what anything is.
Semantic elements are the alternative. They look identical by default - a header and a div render the same - and they differ in what they tell every machine that reads the page.
The landmarks
Five elements carve a page into regions that assistive technology can navigate between.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Semantic layout</title>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header>
<a href="/">Omni Academy</a>
<nav aria-label="Main">
<ul>
<li><a href="/courses">Courses</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main id="main">
<h1>Semantic HTML</h1>
<p>The primary content of the page goes here, once.</p>
</main>
<aside aria-label="Related">
<h2>Related lessons</h2>
<ul>
<li><a href="/lessons/accessibility">Accessibility</a></li>
</ul>
</aside>
<footer>
<p>Omni Academy</p>
</footer>
</body>
</html>
Open this example in the editor and change it. Nothing you type is saved.
header is introductory content - a masthead, or the top of an article. nav is a block of navigation links. main is the page's primary content. aside is tangential: related links, a pull quote, an advertisement. footer is closing information.
Three rules govern them:
One main per page, and it should not be inside any of the others. It is the target of the skip link and the first thing a screen reader user jumps to.
header and footer can repeat. At the top level of the body they mean the page's header and footer; inside an article they mean that article's. The context decides.
Label repeated landmarks. Two nav elements on a page are announced as "navigation" and "navigation" unless aria-label distinguishes them.
A screen reader user can list every landmark and jump straight to one. That is how somebody skips a navigation menu they have already heard on the previous eight pages - and on a page built from div elements there is nothing to list.
Sectioning content
<main>
<h1>Course notes</h1>
<article>
<header>
<h2>Why semantics matter</h2>
<p>Published <time datetime="2026-03-14">14 March 2026</time></p>
</header>
<p>An article is content that would still make sense somewhere else.</p>
<section>
<h3>In a feed</h3>
<p>A section is a thematic grouping that belongs where it is.</p>
</section>
<footer>
<p>Written by Lena.</p>
</footer>
</article>
</main>
Open this example in the editor and change it. Nothing you type is saved.
article is content that is independently distributable: a blog post, a news story, a comment, a product card. The test is whether it would still make sense syndicated into a feed on its own.
section is a thematic grouping within something - a chapter of a document, a part of a form. It is not a generic container: a section should have a heading, and if you cannot think of one, you wanted a div.
The difference catches people out, and the heading test is the reliable version: an article is a thing, a section is part of a thing.
details and summary
A disclosure widget, with no JavaScript at all.
<details>
<summary>What is a void element?</summary>
<p>An element that cannot have content, such as img, br or input.
It has a start tag and no end tag.</p>
</details>
<details open>
<summary>Does this need JavaScript?</summary>
<p>No. The browser handles the toggling, the keyboard and the announcement.</p>
</details>
Open this example in the editor and change it. Nothing you type is saved.
The summary is the always-visible label; everything else is revealed when it opens. It is focusable, it toggles on Enter and Space, it is announced with its expanded state, and open makes it start expanded. A hand-built accordion needs a button, aria-expanded, a keyboard handler and a script to match; this needs two tags.
One limitation worth knowing: details cannot be animated open in most browsers, and content inside a closed details is not findable by the browser's own Find on page in older versions. If either matters, that is the reason to build it by hand.
Time, mark, and the small ones
<article>
<h2>Release notes</h2>
<p>Shipped <time datetime="2026-10-11T09:30">this morning</time>.</p>
<p>The release fixes the <mark>form validation</mark> bug reported last week.</p>
<address>
Questions to <a href="mailto:support@example.com">support@example.com</a>.
</address>
</article>
Open this example in the editor and change it. Nothing you type is saved.
time holds a machine-readable value in datetime while displaying whatever reads naturally. mark highlights text relevant to the reader's current activity - the match in a set of search results, not permanent emphasis. address gives contact details for the nearest article, or for the page when it is not inside one.
Sectioning does not fix your headings
There was once a plan for an outline algorithm: nest a section and its h1 would automatically become a second-level heading, so you could write h1 everywhere and let the structure sort itself out.
It was never implemented by any browser or screen reader, and it has been removed from the specification. The advice that followed from it is still repeated, so it is worth being direct: nesting a section does not change what an h1 means. Heading levels are what you write, and nothing adjusts them.
<main>
<h1>Course notes</h1>
<section aria-labelledby="forms-heading">
<h2 id="forms-heading">Forms</h2>
<p>Still a second-level heading, because it says h2.</p>
</section>
</main>
Open this example in the editor and change it. Nothing you type is saved.
Note aria-labelledby on the section, pointing at its own heading. That is what gives the region a name in the landmark list; a section without one is announced as an unnamed region, which is no more useful than a div.
An element beats a role
ARIA can put a role on any element: role="button", role="navigation", role="main". It exists for the cases where no element fits.
The first rule of ARIA, from the specification itself, is not to use it when an element will do. A button gives you focus, Enter, Space, the disabled state and the announcement. A div role="button" gives you the announcement and nothing else - you now owe tabindex, a keydown handler for two keys, and a focus style, and each is a chance to get it wrong.
Reach for a role when you are building something HTML has no element for. Reach for the element every other time.
What a div is still for
Semantic markup is not a competition to eliminate every div. A div is correct whenever you need a box purely to hang layout or styling on and no element describes what is inside it.
<section aria-labelledby="pricing">
<h2 id="pricing">Pricing</h2>
<div class="grid">
<article class="plan">
<h3>Free</h3>
<p>Everything in the catalogue.</p>
</article>
<article class="plan">
<h3>Team</h3>
<p>Progress reporting for a group.</p>
</article>
</div>
</section>
Open this example in the editor and change it. Nothing you type is saved.
The div there exists to be a CSS grid container. It describes nothing, which is exactly right, because "the thing that makes these sit in two columns" is not a fact about the content.
Practice task
Take any page you have built and mark its regions: header, nav, main, footer, with aside if it has one. Add a skip link at the very top pointing at #main. Then open the accessibility tree in developer tools - in Chrome it is in the Elements panel - and look at the landmark list. If it is empty, nothing on your page has told a screen reader where anything is.