Why does using semantic HTML improve accessibility?
Use elements for their meaning, not appearance: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer> for structure; <button> for clickable actions; <a> for navigation; <label> tied to inputs by id. Semantic HTML gives screen readers landmarks to navigate by, gives keyboard users native focus/activation, and gives search engines structure. Pair with ARIA only when no semantic element fits — and prefer 'no ARIA' over 'wrong ARIA.'
Semantic HTML is the foundation of accessibility. Get it right and you inherit keyboard support, screen reader announcements, and SEO benefits for free. Get it wrong and you spend the next sprint patching it with ARIA.
The structural elements
<body>
<header>
<nav aria-label="Primary"><ul>…</ul></nav>
</header>
<main>
<article>
<h1>Title</h1>
<section>
<h2>Section</h2>
…
</section>
</article>
<aside>Related</aside>
</main>
<footer>…</footer>
</body>Each of these creates a landmark screen readers can jump between (VoiceOver: VO+U → Landmarks). <div> does not.
Interactive elements
| Need | Right element | Wrong (common mistake) |
|---|---|---|
| Click triggers an action | <button> | <div onClick> |
| Navigates to a URL | <a href> | <button onClick={navigate}> |
| Form input + label | <label for> + <input id> | label sibling, no association |
| Group of radio choices | <fieldset><legend> | unstyled divs |
| Expandable disclosure | <details><summary> | div + JS toggle |
| Modal dialog | <dialog> | div + role="dialog" |
<button> gives you: Tab focus, Enter/Space activation, focus ring, disabled state, form submission semantics. <div onClick> gives you none of these — you have to reimplement them all, badly.
Heading hierarchy
One <h1> per page. Don't skip levels (<h1> → <h3>). Screen-reader users navigate by heading (VO+U → Headings), and a broken hierarchy is like a table of contents with missing chapters.
Forms
<label for="email">Email</label>
<input id="email" type="email" required aria-describedby="email-help">
<p id="email-help">We'll never share it.</p>The for/id link is non-negotiable. Without it, clicking the label doesn't focus the input, and screen readers announce "edit text" with no context.
Images
<img src="chart.png" alt="Sales grew 30% Q3 to Q4"> <!-- meaningful -->
<img src="divider.svg" alt=""> <!-- decorative -->Empty alt="" for decorative images. Missing alt entirely makes screen readers read the filename.
ARIA — only when no semantic element fits
The first rule of ARIA: don't use ARIA. <button aria-label="Close"> is fine. <div role="button" aria-label="Close" tabindex="0" onClick={…} onKeyDown={…}> is a smell — that's reimplementing <button>.
ARIA is for things HTML doesn't have: tabs, comboboxes, live regions (aria-live="polite" for toast announcements), expanded/collapsed state on a disclosure trigger (aria-expanded).
Color and focus
- Visible focus indicator on every focusable element — don't
outline: nonewithout a replacement. - Color contrast ≥ 4.5:1 for text, 3:1 for UI components (WCAG AA).
- Never convey meaning by color alone — pair with an icon or text.
Testing
- Tab through the page; can you reach and operate every interactive element?
- Run axe-core or Lighthouse → Accessibility — catches ~30% of WCAG issues.
- Test with a screen reader (VoiceOver Cmd+F5 on Mac, NVDA on Windows) at least once per feature.
Follow-up questions
- •What's the difference between <section> and <article>?
- •When would you use aria-live='polite' vs 'assertive'?
- •How do you handle a custom dropdown accessibly?
- •What's the role of the lang attribute on <html>?
Common mistakes
- •Using <div onClick> instead of <button>, missing all native keyboard and AT support.
- •Skipping heading levels (h1 → h3) for visual sizing — that's what CSS is for.
- •outline: none on focus with no replacement — invisible focus, fails WCAG.
- •Aria-label on elements that already have visible text (overrides it for screen readers).
- •Using <label>Click me</label><input> without a for/id link.
- •alt='image of …' (screen readers already announce 'image'), or missing alt entirely.
Performance considerations
- •Semantic HTML has no runtime cost vs <div> soup — same bytes, same render. The win is in maintainability and zero-cost accessibility. ARIA attributes are cheap; the cost of bad ARIA is correctness, not perf.
Edge cases
- •Icon-only buttons need aria-label (or visually-hidden text) so screen readers announce a name.
- •Decorative images need alt='' (empty), not missing alt.
- •Dynamic content (e.g. error messages appearing after submit) need aria-live regions to be announced.
- •Modal dialogs need focus trap, return focus to opener on close, and Escape to close.
- •Reduced-motion users (@media (prefers-reduced-motion)) — skip animations.
Real-world examples
- •GitHub's UI is largely semantic — try VoiceOver landmark navigation, you can jump straight to the file list, sidebar, or footer.
- •Reddit's old design used <article> for each post — screen reader users could jump post-to-post with one shortcut.
- •Reach UI, Radix UI, React Aria — component libraries built around semantic-first, ARIA-when-needed.