Overview
HTML is the contract between your page and every downstream consumer: screen readers, search crawlers, browser reader modes, embedders, and LLM agents. Semantic elements, correct heading order, and a clean <head> make that contract legible. This page is the baseline every page on a real site should clear.
Pick the element that names the role
Use the element that names what the region is, not a <div> with a class.
<main>: the page’s primary content. One per page.<article>: a self-contained piece of content (a blog post, a product card).<section>: a thematic group within an article or page. Needs a heading.<nav>: site or in-page navigation. Wrap the primary nav and the footer nav.<aside>: tangentially related content (a callout, a sidebar). Not for decoration.<header>and<footer>: scoped to their nearest sectioning element. Per-article headers are fine.
A <div> is a fallback when no element fits, not a default.
One h1, descending order, no skipped levels
Use exactly one <h1> per page. Match it to the page title or hero. Then descend: an <h2> opens a top-level section; an <h3> only inside an <h2>. Do not skip from <h2> to <h4> for styling. Style with CSS; preserve the outline.
Screen readers, reader modes, and crawlers all read the heading tree as the document outline. A broken outline breaks navigation.
Bind labels, set autocomplete, pick the right input type
Every form control gets a real <label>. Use for and id, or wrap the input in the label. Set autocomplete to a known token; the value matters for password managers, mobile autofill, and accessibility.
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="cc-number">Card number</label>
<input id="cc-number" name="cc-number" inputmode="numeric" autocomplete="cc-number" />Pick the input type that matches the data: email, tel, url, number, date. The right type changes the mobile keyboard and the built-in validation. Use inputmode to refine the keyboard without changing validation.
Use aria-label only when no visible label exists
ARIA is a patch, not a primary mechanism. Reach for a native element first. Use aria-label only when a control has no visible text, like an icon button.
<button aria-label="Close dialog">
<svg aria-hidden="true"><!-- icon --></svg>
</button>Never duplicate visible text with aria-label; the visible text already labels the control. Style focus with :focus-visible so keyboard users see the ring without showing it on every mouse click.
Set the head on every page
These five lines belong in every page’s <head>:
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Specific page title - Site name</title>
<meta name="description" content="One sentence; 140 to 160 characters." />
<link rel="canonical" href="https://example.com/this-page" />Add Open Graph tags (og:title, og:description, og:image, og:url, og:type) on any page that might be shared. Pair them with Twitter Card tags for X previews. JSON-LD structured data lives in a <script type="application/ld+json"> block; see structured-data.
Images need alt text and explicit dimensions
Every <img> gets an alt attribute. Use descriptive text for content images. Use alt="" for purely decorative images so screen readers skip them. Never omit the attribute.
Set width and height (in pixels) on every raster image. The browser reserves the space before the bytes arrive, which prevents cumulative layout shift (CLS). For responsive images, use srcset and sizes; the aspect ratio computed from width and height still reserves the slot.
<img
src="/hero-800.jpg"
srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
sizes="(max-width: 720px) 100vw, 800px"
width="800"
height="450"
alt="A subway map of the Tokyo network."
/>For SVG icons, set aria-hidden="true" when an adjacent label already describes them.