← Back to home

4 common semantic anti-patterns — and how to fix them

Semantic HTML mistakes don't belong to any specific framework or library.

They show up in React, Vue, Angular — even in well-structured design systems.

Not because developers don't care, but because the focus shifts to state, logic, and styling… and HTML quietly becomes an afterthought.

The problem?

  • These aren't visual bugs.
  • They pass code review.
  • They ship to production.

But for users relying on keyboard navigation or screen readers, they break core interactions.

Below are 4 of the most common semantic anti-patterns — what they break, and how to fix them.


A visual guide showing four semantic HTML anti-patterns with code examples and their correct alternatives, including clickable div vs button, span vs heading, div-based list vs ul/li, and input without label vs labeled input.


Why this matters (beyond “best practice”)

All examples here directly impact WCAG compliance, especially:

  • 1.3.1 Info and Relationships — structure must be programmatically determinable
  • 2.1.1 Keyboard — all functionality must be keyboard accessible
  • 4.1.2 Name, Role, Value — elements must expose proper semantics

Ignoring semantic HTML is not just a code smell — it's a functional accessibility failure.


The 4 anti-patterns (and fixes)

1 — A <div> with an onClick is not focusable by keyboard and is not announced as interactive by screen readers. A <button> is — by default, for free.

// ❌ Avoid
<div onClick="{handleClick}">Submit</div>

// ✅ Use instead
<button type="button" onClick="{handleClick}">Submit</button>

2 — Screen readers let users jump between headings with a single key. If your headings are <span> elements with a CSS class, that navigation finds nothing.

// ❌ Avoid
<span className="title">Dashboard</span>

// ✅ Use instead
<h1>Dashboard</h1>

3 — Without <ul> and <li>, screen readers cannot announce the number of items or allow users to navigate the list efficiently.

// ❌ Avoid
<div className="list">
  <div>Item one</div>
  <div>Item two</div>
</div>

// ✅ Use instead
<ul>
  <li>Item one</li>
  <li>Item two</li>
</ul>

4 — Placeholder text disappears on focus and is never read as a label. Without a <label for="">, the field has no accessible name.

// ❌ Avoid

<input type="text" placeholder="Email" />

// ✅ Use instead

<label htmlFor="email">Email</label>
<input id="email" type="email" />

Instead of scattering fixes across your codebase, here's a single reference you can copy and apply.