Semantic HTML
The right element carries meaning, keyboard behaviour, and accessibility for free.
25 minDifficulty 1/5html · fundamentalsHand-writtenWritten by a person and not yet reviewed by a second one. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
A `<div>` with a click handler looks identical to a `<button>` once you have styled it. It behaves identically too — for a mouse user on a working trackpad. It is not focusable by keyboard, it does not fire on Enter or Space, it is not announced as a button, it does not appear in a screen reader's list of controls, and it does not go into the disabled state. Choosing the right element is not about tidiness. It is about how much behaviour you get for free versus how much you are about to reimplement badly.
The mental model
Labels on the outside of moving boxes. The box holds the same objects either way, so to you carrying it nothing changes. To everyone else — the movers, the person unpacking, the insurance assessor — a box marked FRAGILE KITCHEN behaves completely differently from an unmarked one. Semantic HTML is writing on the box for all the software that has to handle your page without eyes.
How it works
Elements ship with behaviour, not just meaning
A `<button>` is focusable in tab order, activates on both Enter and Space, fires a click event from the keyboard, exposes a disabled state that also blocks those keyboard activations, and reports itself to assistive technology as a button. An `<a href>` gets all of that plus Ctrl-click to open in a new tab, a context menu with Copy Link, and browser history. None of this is styling — it is behaviour the browser implements, and every bit of it disappears when you use a `<div>`. The practical rule is that you are not choosing a tag, you are choosing whether to inherit a well-tested implementation or write your own.
Headings and landmarks are how non-visual users navigate
A sighted user skims a page by looking at size and position. A screen reader user cannot, so they navigate by structure instead: jump to the next heading, list all headings, jump to the main landmark, cycle through regions. That structure comes from `<h1>` through `<h6>` used in order, and from `<header>`, `<nav>`, `<main>`, `<aside>` and `<footer>`. Choosing an `<h3>` because it looks the right size breaks the outline exactly the way scrambling a table of contents would. Heading level is structure; font size is CSS, and they are unrelated by design.
Every form control needs an associated label
An `<input>` with placeholder text and no label is a control with no accessible name — a screen reader announces it as just 'edit text'. Placeholders also vanish the moment typing starts, which hurts everyone, and their low-contrast default styling fails contrast requirements. The fix is a real `<label>`, associated either by wrapping the input or by matching `for` to the input's `id`. This buys a second thing people forget: clicking the label focuses the control, which makes small checkboxes and radios dramatically easier to hit — on touch screens especially. It is the highest-value four seconds of accessibility work available.
ARIA is a patch, and the first rule is not to use it
The first rule of ARIA, from the specification itself, is: do not use ARIA if a native element with the required semantics already exists. `role="button"` on a div tells assistive technology it is a button while giving it none of a button's behaviour — you still owe `tabindex`, an Enter handler, a Space handler that also prevents scrolling, and `aria-disabled` handling. Now the announcement and the reality can disagree, which is worse than saying nothing, because the user has been promised something the element cannot do. ARIA earns its place for things HTML genuinely lacks — live regions, `aria-expanded` on a custom disclosure, `aria-describedby` linking an error to a field — not as a repair kit for the wrong element.
The mechanism
The same control, twice. Only one of them works: ```html <!-- Announced as: 'generic'. Not focusable. Enter does nothing. --> <div class="btn" onclick="save()">Save</div> <!-- Announced as: 'Save, button'. Tab reaches it. Enter and Space fire. --> <button type="button" onclick="save()">Save</button> ``` To make the first one genuinely equivalent you would need: ```html <div class="btn" role="button" tabindex="0" onclick="save()" onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); save(); }">Save</div> ``` And it still lacks a real `disabled` state, still submits nothing inside a form, and still needs its focus ring restored. Four lines of fragile code to reproduce, incompletely, what one correct element gives you.
flowchart TD
H[Your HTML] --> DOM[DOM tree]
DOM --> RENDER[Render tree<br/>what sighted users see]
DOM --> AX[Accessibility tree<br/>role, name, state]
AX --> SR[Screen reader]
AX --> VC[Voice control]
AX --> KB[Keyboard behaviour]
DIV[div with onClick] -.->|role: generic<br/>name: none| AX
BTN[button] -.->|role: button<br/>name: its text<br/>focusable| AXWhat people get wrong
- Semantic HTML is mainly about SEO.
- It is primarily about behaviour and accessibility. SEO is a side effect. Search ranking is a business reason people use to justify it, which makes it sound optional. Keyboard operability is not optional — in many jurisdictions it is a legal requirement, and it is the difference between a usable product and an unusable one for a real fraction of users.
- Adding role="button" to a div makes it a button.
- It changes what is announced. It adds no behaviour at all. Role affects the accessibility tree only. Focusability, keyboard activation, form submission and the disabled state all remain absent, so the element now claims to be something it cannot do.
- Heading levels should be chosen by how big the text needs to look.
- Levels convey document structure. Size is CSS and completely independent. Screen reader users navigate by heading level and often list all headings to understand a page. Skipping from h1 to h4 for visual reasons corrupts that outline exactly as scrambling a table of contents would.
- A placeholder is good enough as a label.
- A placeholder is not an accessible name, and it disappears when the user starts typing. The control gets announced with no name, so a screen reader user hears only 'edit text'. It also removes the reminder of what a field was for at exactly the moment someone is reviewing a long form before submitting.
- <section> is a semantically better <div>.
- A section without an accessible name adds nothing, and can add noise. section only becomes a navigable region when it has a name, usually via aria-labelledby pointing at its heading. Unnamed sections are announced as unhelpful generic regions. When there is no meaning to convey, a div is the correct, honest choice.
When not to use it
- You need a wrapper purely for layout or styling.
- A div. It is the correct element for 'no meaning here' — reaching for section or article to seem more semantic adds noise to the accessibility tree.
- A widget HTML has no element for — a combobox, a tree view, a tab set.
- The ARIA Authoring Practices pattern for that widget, implemented in full including keyboard interaction. This is where ARIA is correct rather than a patch.
- Complex data visualisation drawn in canvas or SVG.
- A text alternative carrying the same information — a table, a summary, or a downloadable dataset. Pixels have no semantics to recover.
Terms
- Accessibility tree
- — The parallel structure the browser builds from your DOM, exposing role, name and state to assistive technology.
- Role
- — What an element is — button, link, heading, region. Implicit in native elements, overridable with ARIA.
- Accessible name
- — The text a control is announced by. Comes from its content, its label, or an aria-label attribute.
- Landmark
- — A navigable region of the page: header, nav, main, aside, footer. Screen reader users jump between them directly.
- Focusable
- — Reachable via the Tab key. Interactive native elements are focusable by default; a div is not.
- tabindex
- — Controls focusability. Zero puts an element in natural tab order, minus one makes it focusable only by script. Positive values reorder the page and are almost always a mistake.
In an interview
What actually breaks if you use a div with a click handler instead of a button?
- it is not in the tab order, so keyboard users cannot reach it
- Enter and Space do not activate it
- it is announced as generic rather than as a button, and is missing from the screen reader's control list
- there is no disabled state, and no form submission behaviour
- role and tabindex restore the announcement and focus but still not the keyboard activation
When is ARIA the right tool?
- when no native element provides the semantics — tabs, comboboxes, tree views
- for state that HTML cannot express, such as aria-expanded or aria-current
- for live regions announcing asynchronous updates
- for relationships like aria-describedby linking an error message to its field
- never as a substitute for using the correct native element
Can you recall it?
You find a `<div onclick>` acting as a button in a code review. List what is broken, and explain why `role="button"` alone does not fix it.