Semantic markup means choosing HTML elements for what the content is, not how it looks: the site header is header, the menu is nav, the main content is main, headings are h1–h6 without skipped levels, an action is a button, navigation is an a. From that markup the browser builds an accessibility tree that screen readers use to jump between regions and headings. You check the markup with a validator (Nu Html Checker or html-validate) and with the accessibility tree in DevTools.
After this you will be able to
- Mark up a page with
header,nav,main,section,article,asideandfooterand explain which of them become landmarks in the accessibility tree - Build an
h1–h3heading outline without skipped levels and explain why a level is not chosen by font size - Choose between
aandbutton, give imagesalt,widthandheight, and associate form fields withlabel - Check a page with Nu Html Checker,
html-validateand the accessibility tree in DevTools
Verified with
HTML Living Standard (WHATWG, 2026-10-09)html-validate 11.16.2Nu Html Checker 26.10.11Microsoft Edge 155 + Playwright 1.64
Checked against docs: October 11, 2026
Contents
- Semantics: elements by meaning, not by looks
- Document skeleton: doctype, lang, head and body
- Page regions: header, nav, main, section, article, aside, footer
- Headings: one h1 and no skipped levels
- Link or button: how to choose
- Images: alt and dimensions
- Forms: label, input, button
- Checking: validator and accessibility tree
Semantics: elements by meaning, not by looks
In programming, semantics means what a piece of code means. MDN gives a simple example: an h1 tells the browser that its text is a top-level heading, while a span with a large font only looks like one and carries no meaning. Appearance is the job of CSS; HTML describes what each piece of content is.
Below is a "div soup": a page built from anonymous div elements. CSS can make it look fine, but for machines there is no header, no menu, no main heading and no button.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Coffee machine repair</title>
</head>
<body>
<div class="header">
<img src="/img/logo.svg">
<div class="menu">
<a href="#" onclick="openServices()">Services</a>
</div>
</div>
<div class="hero">
<div class="title">Coffee machine repair in Almaty</div>
<h3>Diagnostics and repair for coffee shops</h3>
<div class="btn" onclick="sendForm()">Send request</div>
<input placeholder="Phone">
</div>
</body>
</html>Who reads markup besides people? The browser turns HTML into the DOM, a tree of elements, and from it builds the accessibility tree, where each node has a role (button, link, heading), a name and a state. Screen readers and other assistive technology work with that tree: they jump between page regions and headings and announce "button" or "link". According to MDN, browser reader modes look for main, headings and sections. MDN also lists search engines among those that benefit from meaningful markup. And a developer finds a block faster among nav and footer than among a dozen nested div elements.
Div soup
- A screen reader sees loose text and a single link with no structure
- A
div"button" never receives keyboard focus - There is no level-one heading, so it is unclear what the page is about
- Every behaviour has to be rebuilt in JavaScript
Semantic markup
- Header, menu, main content and footer are separate regions
- A
buttonis reachable with Tab and fires on Enter and Space - Headings
h1–h3form the page outline - The browser provides behaviour for free: focus, form submission, navigation
Document skeleton: doctype, lang, head and body
Every page starts with the same skeleton. It is short, but each line switches something on or off in the browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Coffee machine repair in Almaty — Zerno service</title>
<meta name="description" content="Diagnostics and repair of coffee machines for coffee shops and offices in Almaty.">
</head>
<body>
<h1>Coffee machine repair in Almaty</h1>
</body>
</html><!DOCTYPE html>
- Why it is there
- Turns on standards mode. Without it the browser falls back to quirks mode, a compatibility mode for old sites, and computes some CSS the old way
<html lang="en">
- Why it is there
- The page language. Screen readers use it to pick pronunciation; WCAG 3.1.1 requires the page language to be programmatically determinable
<meta charset="utf-8">
- Why it is there
- The document encoding; without it non-Latin text may turn into garbage characters
<meta name="viewport" …>
- Why it is there
- Without it a mobile browser renders the page in a virtual window wider than the screen and shrinks it as a whole
<title>
- Why it is there
- The name in the tab and in bookmarks; search engines use it as one of their signals
<meta name="description">
- Why it is there
- A short summary of the page content
<body>
- Why it is there
- Everything the user sees and hears
Page regions: header, nav, main, section, article, aside, footer
Large parts of the page are marked up with sectioning elements. Several of them become landmarks in the accessibility tree: a screen reader can list them and jump to one in a single step.
header
- What it means
- Introductory content: logo, menu, search. Inside an
articleorsectionit is that block's header - Role in the accessibility tree
bannerwhen theheaderis not nested inmain,article,section,asideornav
nav
- What it means
- A major block of navigation links. Not every group of links is a
nav - Role in the accessibility tree
navigation
main
- What it means
- The dominant content of the page, not repeated on other pages
- Role in the accessibility tree
main; a document has only one visiblemain
section
- What it means
- A thematic section with a heading when no more specific element fits
- Role in the accessibility tree
region, but only when the section has an accessible name, for example viaaria-labelledby
article
- What it means
- A self-contained piece: a blog post, a comment, a product card
- Role in the accessibility tree
article
aside
- What it means
- Indirectly related content: a call-out, related links
- Role in the accessibility tree
complementary
footer
- What it means
- The footer of the page or a block: contacts, authorship, copyright
- Role in the accessibility tree
contentinfofor the page footer
div does not go away. The HTML standard says plainly that section is not a generic container and that wrappers for styling or scripting should be a div. A quick test: if a block has its own heading and belongs in a table of contents, it is a section; if it exists only for a grid or a background, it is a div. The standard puts the difference between section and article like this: a section forms part of something else, an article is its own thing.
Let's build a landing page for a coffee machine repair service: header with menu, hero, services, benefits, request form and footer. The company is fictional; the address and email are placeholders.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Coffee machine repair in Almaty — Zerno service</title>
<meta name="description" content="Diagnostics and repair of coffee machines for coffee shops and offices in Almaty.">
</head>
<body>
<a href="#main">Skip to content</a>
<header>
<a href="/"><img src="/img/logo.svg" alt="Zerno — home" width="120" height="32"></a>
<nav aria-label="Main menu">
<ul>
<li><a href="#services">Services</a></li>
<li><a href="#benefits">Why us</a></li>
<li><a href="#request">Request</a></li>
</ul>
</nav>
</header>
<main id="main">
<section>
<h1>Coffee machine repair in Almaty</h1>
<p>Diagnostics, descaling and repair of espresso and bean-to-cup machines for coffee shops and offices.</p>
<a href="#request">Request a repair</a>
<img src="/img/master.jpg" alt="A technician replaces the group head gasket of an espresso machine" width="1200" height="800">
</section>
<section id="services">
<h2>Services</h2>
<article>
<h3>Diagnostics</h3>
<p>We find the cause of the fault and agree on the cost before any work starts.</p>
</article>
<article>
<h3>Descaling</h3>
<p>We flush the water system and replace filters as the manufacturer recommends.</p>
</article>
<article>
<h3>Repair</h3>
<p>We replace pumps, gaskets, sensors and control boards.</p>
</article>
</section>
<section id="benefits">
<h2>Why coffee shops trust us</h2>
<ul>
<li>On-site visits across the city</li>
<li>Genuine spare parts</li>
<li>Warranty on all work</li>
</ul>
</section>
<section id="request">
<h2>Repair request</h2>
<form action="/api/request" method="post">
<label for="name">Name</label>
<input id="name" name="name" type="text" autocomplete="name" required>
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" required>
<label for="problem">What is wrong with the machine</label>
<textarea id="problem" name="problem" rows="4"></textarea>
<button type="submit">Send request</button>
</form>
</section>
</main>
<footer>
<address>
1 Example St, Almaty · <a href="mailto:hello@example.com">hello@example.com</a>
</address>
<small>© 2026 Zerno service</small>
</footer>
</body>
</html>- The first link, "Skip to content", points to
main id="main": keyboard users skip the menu with one Enter. We checked that it is the first element Tab lands on. - The
navhas anaria-label: when a page has several menus (header and footer), a screen reader tells them apart by name. - Service cards are
articleelements: each is self-contained and could live on its own page. Benefits are a plainullist. - The logo inside the link has an
altthat says where the link goes, not "logo". addressin the footer holds the page owner's contact details.
Headings: one h1 and no skipped levels
The h1–h6 elements define six heading levels. All headings in document order make up the page outline, and many screen reader users explore a page exactly that way, jumping from heading to heading. So levels follow the structure, not the font size: size is CSS's job.
- The HTML standard: each following heading may be at the same level, higher, or exactly one level deeper than the previous one.
h1→h3without anh2is an error, not a style choice. - If a page has headings, at least one must be level 1.
- MDN recommends one
h1per page that describes the content, much liketitle. The standard allows severalh1elements, but it is not a best practice. - The old advice "give every
sectionits ownh1" is outdated: browsers never implemented the algorithm that recalculated those levels, and nestedh1elements in sections are now non-conforming.
1
- Heading
- Coffee machine repair in Almaty
2
- Heading
- Services
3
- Heading
- Diagnostics · Descaling · Repair
2
- Heading
- Why coffee shops trust us
2
- Heading
- Repair request
Link or button: how to choose
The rule is short: a link a goes to an address — another page, an anchor, a file. A button performs an action on the current page — opens a dialog, shows reviews, submits a form. You can style them alike, but they behave differently.
<h1>Links and buttons</h1>
<!-- Going to another page or an anchor — a link -->
<a href="/prices">Repair prices</a>
<a href="#request">Request a repair</a>
<!-- An action on the same page — a button with type="button" -->
<button type="button" id="show-reviews">Show all reviews</button>
<!-- Submitting a form — a type="submit" button inside the form -->
<form action="/api/callback" method="post">
<label for="cb-phone">Phone</label>
<input id="cb-phone" name="phone" type="tel" autocomplete="tel" required>
<button type="submit">Call me back</button>
</form><body> fragment of the verified links-buttons.html page.MDN specifically warns against "link buttons" such as href="#" or javascript:void(0) with a click handler: they break copying the link, opening it in a new tab and bookmarking, misbehave while JavaScript is loading, and expose the wrong role to screen readers. That is what button is for.
A div with onclick is even worse. We tabbed through both pages in Edge: on the bad one focus lands on the link and the input and skips the div "button", so it cannot be pressed from the keyboard. On the good one the button receives focus and fires on both Enter and Space. Adding role="button" to a div does not fix this: as web.dev explains, the role only changes what the screen reader announces, while focus and keys would have to be scripted by hand.
Images: alt and dimensions
The alt attribute is the text replacement for an image. A screen reader reads it, and the browser shows it if the image fails to load. MDN advises describing what the image means rather than the fact that it exists: not "photo", not the file name, but what matters in context. If an image is purely decorative, leave alt empty so assistive technology skips it. If the image sits inside a link or a button, alt describes the action or destination.
<h1>Images</h1>
<!-- Informative: alt conveys what matters in the photo -->
<img src="/img/master.jpg" alt="A technician replaces the group head gasket of an espresso machine" width="1200" height="800">
<!-- Decorative: empty alt, screen readers skip it -->
<img src="/img/beans-pattern.svg" alt="" width="400" height="80">
<!-- Image link: alt says where the link goes -->
<a href="/"><img src="/img/logo.svg" alt="Zerno — home" width="120" height="32"></a><body> fragment; the page includes the CSS rule img { max-width: 100%; height: auto; }.The accessibility tree of this page contains two images, not three: the browser dropped the decorative pattern with alt="", and the logo link took its name, "Zerno — home", from alt.
The width and height attributes are the file's intrinsic size in pixels, without units. From them the browser computes the aspect ratio in advance and reserves space, so text does not jump when the image arrives. Together with CSS max-width: 100%; height: auto the image stays fluid. We delayed an image by 3 seconds and measured its box before it loaded: on a 375 px wide screen a 1200×800 photo already had 375×250 px reserved, while the same image without the attributes had a height of 0.
Forms: label, input, button
Every field needs a label that is associated with it in code. There are two ways: explicit, where the label's for equals the field's id, and implicit, where the field sits inside the label. The association does two things: a screen reader announces the label when the field gets focus, and clicking the label focuses the field, which gives a checkbox a bigger hit area.
<h1>Descaling reminders</h1>
<form action="/api/subscribe" method="post">
<!-- Explicit association: label's for = the field's id -->
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<!-- Implicit association: the field is inside the label -->
<label>
<input name="consent" type="checkbox" required>
I agree to receive service emails
</label>
<button type="submit">Subscribe</button>
</form><body> fragment of the verified form.html page.nameis the key the value is sent under. A field withoutnameis not submitted.typechanges behaviour:emailchecks the address format before submission — in our check the form withnot-an-emailwas not sent to the server, and with a valid address it was. On phonestelmay bring up a numeric keypad.autocompletetells the browser what data is expected (name,email,tel) so it can offer saved values.requiredblocks submitting an empty field. It is a convenience for the user, not protection: the server must validate data on its own anyway.- The submit button is a
button type="submit"inside theform. Then the form submits both on click and on Enter in a field.
Which fields to keep, where to show errors and how to shorten a form are covered in the UX article on forms; here we deal only with markup.
Checking: validator and accessibility tree
W3C validator
Open Nu Html Checker at validator.w3.org/nu and paste the code or the page address. It finds syntax errors and violations of the standard: an image without
alt, invalid nesting.html-validate in the project
The same kind of checks locally and in CI:
npm install --save-dev html-validate, a config file and a run vianpx. Thedocumentpreset adds heading-level checks.Accessibility tree in DevTools
In Chrome DevTools: Elements → Accessibility tab → Show accessibility tree. The DOM is replaced with a tree of roles and names, so you see the regions, headings and field labels at once.
Keyboard
Walk through the page with Tab. Chrome's documentation says plainly that DevTools cannot check this for you: focus must move in order and reach every interactive element.
{
"extends": ["html-validate:recommended", "html-validate:document"]
}$ npx html-validate bad.html
…\bad.html
9:6 error <img> is missing required "alt" attribute wcag/h37
16:6 error Initial heading level must be <h1> but got <h3> heading-level
18:6 error <input> is missing recommended "type" attribute no-implicit-input-type
18:6 error <input> element does not have a <label> input-missing-label
✖ 4 problems (4 errors, 0 warnings)
More information:
https://html-validate.org/rules/wcag/h37.html
https://html-validate.org/rules/heading-level.html
https://html-validate.org/rules/no-implicit-input-type.html
https://html-validate.org/rules/input-missing-label.html
$ echo $?
1
$ npx html-validate index.html
$ echo $?
0Notice what the validator did not find: the div with onclick and the href="#" link. They are syntactically valid; only the accessibility tree and a keyboard check catch them. Here is the accessibility tree of our landing page, captured in Edge with Playwright's ariaSnapshot(); DevTools shows the same roles and names:
- link "Skip to content":
- /url: "#main"
- banner:
- link "Zerno — home":
- /url: /
- img "Zerno — home"
- navigation "Main menu":
- list:
- listitem:
- link "Services":
- /url: "#services"
- listitem:
- link "Why us":
- /url: "#benefits"
- listitem:
- link "Request":
- /url: "#request"
- main:
- heading "Coffee machine repair in Almaty" [level=1]
- paragraph: Diagnostics, descaling and repair of espresso and bean-to-cup machines for coffee shops and offices.
- link "Request a repair":
- /url: "#request"
- img "A technician replaces the group head gasket of an espresso machine"
- heading "Services" [level=2]
- article:
- heading "Diagnostics" [level=3]
- paragraph: We find the cause of the fault and agree on the cost before any work starts.
- article:
- heading "Descaling" [level=3]
- paragraph: We flush the water system and replace filters as the manufacturer recommends.
- article:
- heading "Repair" [level=3]
- paragraph: We replace pumps, gaskets, sensors and control boards.
- heading "Why coffee shops trust us" [level=2]
- list:
- listitem: On-site visits across the city
- listitem: Genuine spare parts
- listitem: Warranty on all work
- heading "Repair request" [level=2]
- text: Name
- textbox "Name"
- text: Phone
- textbox "Phone"
- text: What is wrong with the machine
- textbox "What is wrong with the machine"
- button "Send request"
- contentinfo:
- text: 1 Example St, Almaty ·
- link "hello@example.com":
- /url: mailto:hello@example.com
- text: © 2026 Zerno service<!DOCTYPE html>,langonhtml,charset,viewportand a meaningfultitleare in place- Header, menu, main content and footer use
header,nav,main,footer - One
h1, heading levels without gaps - Navigation uses links with real addresses, actions use
buttonwith the righttype - Informative images have a meaningful
alt, decorative ones an emptyalt;widthandheightare set - Every field has an associated
label, anameand a fittingtype - The validator reports no errors, Tab reaches every interactive element
Next comes layout: placing these blocks on a grid with Flexbox and Grid without touching the markup. Accessibility in code — focus, ARIA and screen reader testing — has its own lesson in this track.
How this was verified3 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.