← Back to home

4 common semantic anti-patterns and how to fix them

Updated

Semantic HTML mistakes don't belong to any one framework or library. They show up in React, Vue and Angular, and even in well-structured design systems.

Developers care. The focus just moves to state, logic and styling, and HTML becomes an afterthought.

These mistakes aren't visual bugs, so they pass code review and ship to production. For users who rely 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

Every example here affects 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 causes functional accessibility failures.


The 4 anti-patterns and their fixes

  1. A <div> with an onClick can't be focused by keyboard, and screen readers don't announce it as interactive. A <button> gets both by default.
// ❌ Avoid
<div onClick="{handleClick}">Submit</div>

// ✅ Use instead
<button type="button" onClick="{handleClick}">Submit</button>
  1. 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>
  1. Without <ul> and <li>, screen readers can't announce the number of items or let users move through 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>
  1. 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" />
LinkedInX