---
title: "Semantic HTML"
slug: "html"
category: "frontend"
tags: ["frontend", "html", "accessibility"]
status: "stable"
last_updated: 2026-05-14
summary: "Semantic elements, heading order, accessible forms, head and meta hygiene, and image defaults that prevent layout shift."
related: ["[[frontend/css]]", "[[frontend/tailwind]]", "[[seo/technical]]", "[[seo/structured-data]]"]
---

## 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. See [[frontend/forms]] for the full form playbook.

```html
<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. See [[frontend/accessibility]] for the wider rules on when ARIA earns its keep.

```html
<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>`:

```html
<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 [[seo/structured-data]] and [[howto/add-jsonld-to-static-site]] for the static-site embed walkthrough.

## 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); see [[seo/core-web-vitals]] for the CLS thresholds and [[seo/image-seo]] for the full image checklist. For responsive images, use `srcset` and `sizes`; the aspect ratio computed from `width` and `height` still reserves the slot.

```html
<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.

## Related

- [[frontend/css]]
- [[frontend/tailwind]]
- [[seo/technical]]
- [[seo/structured-data]]
