Writing Meaningful HTML: 7 Semantic HTML Mistakes Almost Everyone Makes
They look identical on screen, but a div button, an input without a label, or a heading picked by font size look completely different to keyboard users, screen readers, and search engines. Common semantic HTML mistakes and how to fix them, with examples you can try.
They look the same, so what's different?
The preview below has two cards. One is built entirely from divs; the other uses tags that match their meaning. They look exactly the same. Compare the code in the html tab below.
Both work fine with a mouse. But click once inside the preview and press Tab. Focus only goes to the button in the second card. The div button can't be reached from the keyboard at all.
Semantic HTML means choosing tags by "what it is," not "how it looks." CSS handles appearance; tags handle meaning.
To someone looking at the screen there's no difference. To anything that reads the HTML, the difference is big.
| Who reads your HTML | What meaningful tags give them |
|---|---|
| Keyboard users | They can Tab between buttons, links, and inputs |
| Screen readers | They announce roles like "button," "heading level 2," "navigation" |
| Search engines | They can tell main content, headings, and navigation apart |
| Browsers | Reader mode, auto-translate, and autofill work better |
| The next developer | header reads faster than div class="header" |
Let's go through the mistakes you see most often in real code.
Mistake 1. A clickable div
The most common mistake, and the one with the biggest impact.
<!-- ❌ -->
<div class="btn" onclick="save()">Save</div>
<!-- ✅ -->
<button type="button" onclick="save()">Save</button>
Faking a button properly with a div takes all of this:
- tabindex="0" so it can receive keyboard focus
- role="button" so screen readers know it's a button
- Handling the Enter and Space keys yourself
- Implementing a disabled state yourself
A button tag gives you all of it by default. Click inside the preview below, move focus with Tab, and press Enter.
A button inside a form defaults to type="submit". If the button shouldn't submit the form, always write type="button", or every press will submit it.
Mistake 2. Mixing up links and buttons
a and button are both "things you press," so they're easy to confuse, but the rule is simple.
If it takes you somewhere else, it's an a. If it does something on the current page, it's a button.
<!-- ❌ Using a link for something that doesn't navigate -->
<a href="#" onclick="openModal()">Log in</a>
<!-- ✅ It acts on the current page, so it's a button -->
<button type="button" onclick="openModal()">Log in</button>
<!-- ❌ Using a button to navigate -->
<button onclick="location.href='/pricing'">See pricing</button>
<!-- ✅ It navigates, so it's a link -->
<a href="/pricing">See pricing</a>
Turn a link into a button and open in new tab, copy link address, and search engine crawling all stop working. Use an href="#" link as a button and the page jumps to the top or the URL gets a # tacked on.
Mistake 3. Picking heading tags by font size
"This needs to be small, so h4" is a very common choice. But h1–h6 build the document's outline. Screen reader users skim pages by listening to headings only.
<!-- ❌ Picked by size, so the levels are all over the place -->
<h1>Shop</h1>
<h4>Today's picks</h4>
<h2>Product name</h2>
<!-- ✅ Follow the structure, size with CSS -->
<h1>Shop</h1>
<h2 class="section-title">Today's picks</h2>
<h3>Product name</h3>
- It's safest to have a single h1 describing the page's topic.
- Don't skip levels (h2 straight to h4 ❌).
- To change the size, change the CSS, not the tag.
Mistake 4. Inputs without a label
People often drop the label and rely on placeholder because it looks cleaner. In the preview, click the text next to each checkbox.
The first checkbox, with no label, only checks if you hit the tiny box exactly. With a label, clicking the text checks it too, and clicking the word Email puts the cursor straight into the input. On mobile the difference is even bigger.
Why placeholder alone isn't enough:
- It disappears once you start typing, so you forget what the field was for.
- Its faded color is hard to read.
- Screen readers don't always announce it as the field's name.
If the design calls for no visible label, don't delete it. Hide it visually with CSS instead (a class commonly named sr-only). Sighted users won't see it, but screen readers can still read it.
Mistake 5. Missing or meaningless alt text
alt is the text that stands in for an image when it can't be seen: by screen readers, when the image fails to load, and by search engines.
| Situation | What to write |
|---|---|
| Image that conveys content | Describe what it shows: alt="Bar chart showing Q3 revenue up 20%" |
| Decorative image (background patterns, flourishes) | Empty: alt="" (screen readers skip it) |
| Icon inside a link or button | Describe the action: alt="Go to cart" |
<!-- ❌ With no alt, some screen readers read out the file name ("img underscore zero one two three dot png") -->
<img src="img_0123.png" />
<!-- ❌ Meaningless alt -->
<img src="chart.png" alt="image" />
<!-- ✅ -->
<img src="chart.png" alt="Bar chart showing Q3 revenue up 20%" />
<img src="divider.svg" alt="" />
Leaving out the alt attribute and setting it to empty (alt="") are different things. Empty means "this is decoration, skip it." Missing means "someone forgot."
Mistake 6. Building lists out of divs
Menus, tag lists, search results: when the same kind of thing repeats, it's a list.
<!-- ❌ -->
<div class="menu">
<div><a href="/">Home</a></div>
<div><a href="/blog">Blog</a></div>
<div><a href="/about">About</a></div>
</div>
<!-- ✅ -->
<nav aria-label="Main menu">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
With list tags, screen readers announce "list, 3 items" up front, so users know how long the list is before they start. If order matters (install steps, rankings), use ol.
Mistake 7. Building the page skeleton entirely from divs
Tags like header, nav, main, and footer are called landmarks. Screen reader users use them to jump around: "skip to main content," "go to navigation." Think of them as the numbered exits of a subway station.
| If you're writing this | Write this instead | Meaning |
|---|---|---|
| <div class="header"> | <header> | Site logo, top area |
| <div class="nav"> | <nav> | Main menu, groups of navigation links |
| <div class="main"> | <main> | The page's core content (one per page) |
| <div class="post"> | <article> | Self-contained content (a post, a comment, a product card) |
| <div class="section"> | <section> | A thematic group with its own heading |
| <div class="sidebar"> | <aside> | Related but secondary content |
| <div class="footer"> | <footer> | Copyright, contact info, and other bottom matter |
<body>
<header>
<a href="/">Logo</a>
<nav aria-label="Main menu">...</nav>
</header>
<main>
<article>
<h1>Post title</h1>
<section>
<h2>First subtopic</h2>
<p>...</p>
</section>
</article>
<aside>Related posts</aside>
</main>
<footer>© 2026</footer>
</body>
Going the other way and turning every div into a section is also a mistake. Use section only for a meaningful unit with a heading. If it's just a wrapper for styling, div is correct. div isn't a bad tag; it means "a box with no special meaning."
ARIA is a last resort
Once you start caring about accessibility, you'll run into ARIA attributes like role and aria-label. They're powerful, but they come with a first rule:
If a native HTML element does the job, use that element instead of ARIA.
<button> beats <div role="button" tabindex="0">, and <nav> beats <div role="navigation">. ARIA only says "this is a button"; it doesn't add any keyboard behavior. Wrong ARIA is more confusing than no ARIA at all.
Is my page OK? A 3-minute check
| How | What to check |
|---|---|
| Put the mouse down and use only Tab | Can you reach every button and link? Is the focus outline visible? |
| Dev tools → Lighthouse → Accessibility audit | Missing alt, missing label, color contrast issues |
| Dev tools → Elements → Accessibility tab | What role and name each element is announced with |
| List just the headings (run the code below in the console) | Are any heading levels skipped? |
// Paste into the console to print the page's heading structure, indented
document.querySelectorAll("h1,h2,h3,h4,h5,h6").forEach((h) =>
console.log(" ".repeat((h.tagName[1] - 1) * 2) + h.tagName + " " + h.textContent.trim())
);
Summary: why this is worth knowing
| Mistake | Fix |
|---|---|
| Clickable div | button |
| href="#" links that run actions | Navigation is a, actions are button |
| Headings picked by font size | Follow the structure h1→h2→h3, size with CSS |
| Inputs with only a placeholder | Connect a label |
| Missing or meaningless alt | Describe the content, or alt="" for decoration |
| Lists made of divs | ul/ol + li |
| Page skeleton made only of divs | header, nav, main, article, footer |
Semantic HTML isn't a separate "accessibility task" you set time aside for. It's the habit of thinking once more when you pick a tag. That one habit lets keyboard and screen reader users actually use your site, helps search engines understand your content, and lets the teammate who reads your code later see its structure at a glance. Next time you're about to type div, ask yourself first: "Is this really a box with no meaning?"