Semantic HTML
Why semantic HTML matters: the accessibility tree
Alongside the DOM and CSSOM, the browser builds a third structure, the accessibility tree. Each node in it carries a role (what this is), a name (what to call it), and a state (checked, expanded…). Screen readers and other assistive tech consume that tree, not your pixels. Native elements fill in all three for free: <button>Save</button> arrives as role button, name "Save", focusable. A <div> contributes nothing, whatever it looks like.
Pick the element with the right meaning and the browser ships the behavior and the accessibility for free; SEO, reader mode, and readable code come along as side effects. When native elements can't express a widget, ARIA patches the gap, and that repair work lives in the accessibility module. At this band the rule is native first.
How screen reader users navigate a page
Screen-reader users rarely read a page top to bottom. They jump between landmarks and headings the way you visually scan. Two structures to get right:
- Landmarks.
<header>,<nav>,<main>(exactly one),<aside>,<footer>. A page whose entire scaffold isdiv.header,div.sidebar,div.contenthas zero jump targets. - Headings.
h1throughh6form the document outline. Oneh1for the page topic, then descend without skipping levels: anh2whose subsections areh4s reads like a chapter with a missing section. ❌ Never pick a heading level for its font size; font size is a CSS job.
❌ "Multiple h1s are invalid HTML": folklore, they validate fine. The full story waits until middle depth; the junior-safe rule is one h1 with real levels below it.
When to use button, a, or div
<button>does something on this page. Free: keyboard focus, activation on Enter and Space, rolebutton, form participation. One trap: inside a form itstypedefaults tosubmit, so writetype="button"for anything that shouldn't submit.<a href>goes somewhere. Free: open in new tab, copy link, middle-click, visited state, link role. An<a>withouthref(or withhref="#"and a click handler) is a button in costume and loses everything links are for.<div onclick>is neither. It can't take keyboard focus, ignores key presses, and is invisible to the accessibility tree, so keyboard users can't reach it and screen readers can't announce it. Styling was the only thing it had in common with a button.
❌ "Once it's styled the same, a div with a click handler is a button." It's a picture of a button.
How to label form inputs
Every input needs an associated <label>, either through for/id or by wrapping:
<label for="email">Work email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
The association buys two things: clicking the label focuses the control (a much bigger tap target), and the label text becomes the input's accessible name. ❌ "The placeholder already labels it": a placeholder vanishes on the first keystroke, is not a click target, and is not a reliable accessible name. Treat it as a hint ("name@company.com") that sits alongside a real label.
type does work too: email, tel, url, number, and date each summon the matching mobile keyboard plus basic built-in validation, and password gets you real masking and password-manager integration. autocomplete tokens (email, name, new-password) are what lets browsers autofill. Every type="text" with a placeholder is usually one of these, misdeclared.
What the form element gives you for free
A real <form> is more than a wrapper. You get implicit submission (Enter in a text field submits), one interceptable submit event instead of a click handler on a fake button, and the autofill heuristics that password managers key off. On submit, named controls serialize as name/value pairs, logic you never have to write.
Groups need <fieldset> + <legend>. For a radio/checkbox cluster, the legend is announced together with each option's own label ("Notification channel: Email"). A styled div above the group is visually identical and programmatically attached to nothing, so a screen-reader user tabbing into the third radio hears "SMS" with no idea what question it answers.
Constraint validation runs deeper than the popups: required, pattern, min/max, minlength, and the type itself define validity the browser enforces and exposes. Style with :user-invalid (matches only after interaction) instead of :invalid, which matches on first paint and turns fields red before the user types. Taking over the UI? novalidate on the form disables the native popups but keeps the whole validity API and pseudo-classes. Your custom error message must still be associated with its field; aria-describedby pointing at the error's id is the baseline, and live announcement plus focus handling for errors belong to the accessibility module. All of this is UX, not security: validate on the server regardless.
Native dialog and details elements
<dialog> + showModal() is a shipped modal: rendered in the top layer (no z-index war), a stylable ::backdrop, Esc to close, and everything outside made inert, which is the focus trap you would otherwise hand-roll. A <form method="dialog"> inside closes the dialog with the button's value as returnValue, a confirm dialog with zero JS wiring. ❌ Don't toggle the open attribute by hand; that opens the non-modal variant with none of the above. You still owe design work: autofocus on the right control, focus return on close. Details in the accessibility module.
<details>/<summary> is a free disclosure widget: pointer and keyboard toggling plus exposed expanded/collapsed state, without any JS. Give several the same name and you have an exclusive accordion; react to state changes via the toggle event. Check what these already cover before rebuilding either widget on divs.
Media: alt text, figure, time, track
Alt text is judged by context rather than raw accuracy. The same photo is alt="Fluffy" as an avatar and alt="Small terrier mix with a white chest, sitting" on an adoption page. Rules that survive review:
- Ask "what would I say instead of the image if I were reading the page aloud?"
- Never "image of…": the role already says it's an image.
- Decorative images get
alt=""(announced as nothing). Omitting the attribute is worse than it sounds, because screen readers then fall back to reading the file name.alt=""is a decision; no alt is a bug. - Alt and
<figcaption>answer different questions: alt replaces the image, a caption comments on it, and writing one does not excuse skipping the other.<figure>binds media and caption into one unit.
<time datetime="2026-07-09">last Thursday</time> keeps prose human and machine-parseable. <video> wants a <track>: kind="captions" carries dialog plus sound cues for viewers without audio, while kind="subtitles" is translation and assumes the viewer can hear; each track takes srclang and label, and one gets default. Every <iframe> needs a title, or the embed announces as an anonymous hole in the page. (Responsive images and loading behavior, srcset and loading=lazy, are performance topics covered in loading-performance.)
Layout tables are dead. Data tables are the opposite of obsolete: only <table> markup exposes cell↔header relationships. <caption> names the table, and <th scope="col">/<th scope="row"> get re-announced with each data cell ("EMEA, Revenue, $4.2M"). A CSS-grid div matrix looks identical and reads as disconnected strings. (headers/id exist for spanning monsters, which are usually a sign to simplify the table.)
When a div is the right element
Semantics describe meaning that exists. A flex row, a card shadow, and a spacing wrapper have none, so <div>/<span> are the correct, honest elements for pure layout and styling hooks. Forcing <section> onto every box helps nobody: an unnamed section maps to no landmark and behaves exactly like a div in the accessibility tree. Naming one promotes it to a region, a step to take on purpose or not at all. Landmark soup is div soup with better PR: a page with nine <nav>s is as unnavigable as one with none.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.