Skip to main content
Skip to docs content

Common Mistakes

Real-world HTML patterns that break screen reader experiences — and how to fix them. Each example shows the bad markup, what screen readers actually announce, and the corrected version.

Button with no accessible name

Icon-only buttons without text or aria-label are announced as just "button" — users have no idea what it does.

Bad
HTML — Don't do this
<button>
  <svg viewBox="0 0 24 24"><!-- icon --></svg>
</button>
NVDA announces
"button"
VoiceOver announces
"button"

Screen readers announce "button" with no context. The user has to guess what this button does. This is one of the most common accessibility failures.

Fixed
HTML — Do this instead
<button aria-label="Close dialog">
  <svg viewBox="0 0 24 24" aria-hidden="true"><!-- icon --></svg>
</button>
NVDA now announces
"Close dialog, button"
VoiceOver now announces
"Close dialog, button"

Skipped heading levels

Jumping from h1 to h3 breaks the document outline. Screen reader users navigate by heading level and expect a logical hierarchy.

Bad
HTML — Don't do this
<h1>Dashboard</h1>
<h3>Recent Activity</h3>
<h3>Settings</h3>
NVDA announces
"Dashboard, heading level 1
Recent Activity, heading level 3
Settings, heading level 3"
VoiceOver announces
"heading level 1, Dashboard
heading level 3, Recent Activity
heading level 3, Settings"

Users navigating by heading level will look for h2 and find nothing. The jump from h1 to h3 suggests missing content. Speakable's audit report flags this as an error.

Fixed
HTML — Do this instead
<h1>Dashboard</h1>
<h2>Recent Activity</h2>
<h2>Settings</h2>
NVDA now announces
"Dashboard, heading level 1
Recent Activity, heading level 2
Settings, heading level 2"
VoiceOver now announces
"heading level 1, Dashboard
heading level 2, Recent Activity
heading level 2, Settings"

Duplicate landmarks without labels

Multiple nav elements without aria-label are indistinguishable to screen reader users.

Bad
HTML — Don't do this
<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>
<!-- ... page content ... -->
<nav>
  <a href="/privacy">Privacy</a>
  <a href="/terms">Terms</a>
</nav>
NVDA announces
"navigation landmark
  Home, link
  About, link
navigation landmark
  Privacy, link
  Terms, link"
VoiceOver announces
"navigation
  Home, link
  About, link
navigation
  Privacy, link
  Terms, link"

Both navs are announced identically. When a user opens the landmarks list, they see two "navigation" entries with no way to tell them apart.

Fixed
HTML — Do this instead
<nav aria-label="Main navigation">
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>
<!-- ... page content ... -->
<nav aria-label="Footer links">
  <a href="/privacy">Privacy</a>
  <a href="/terms">Terms</a>
</nav>
NVDA now announces
"Main navigation, navigation landmark
  Home, link
  About, link
Footer links, navigation landmark
  Privacy, link
  Terms, link"
VoiceOver now announces
"navigation, Main navigation
  Home, link
  About, link
navigation, Footer links
  Privacy, link
  Terms, link"

Image without alt text

Images without alt attributes are announced with their filename or as just "image" — meaningless to screen reader users.

Bad
HTML — Don't do this
<img src="/images/team-photo.jpg" />
NVDA announces
"graphic"
VoiceOver announces
"image"

The user knows there's an image but has no idea what it shows. For informative images, always provide descriptive alt text. For decorative images, use alt="" to hide them from screen readers.

Fixed
HTML — Do this instead
<!-- Informative image -->
<img src="/images/team-photo.jpg"
  alt="The Speakable team at a conference booth" />

<!-- Decorative image -->
<img src="/images/divider.svg" alt="" />
NVDA now announces
"The Speakable team at a conference booth, graphic"
VoiceOver now announces
"The Speakable team at a conference booth, image"

Form input without label

Inputs without associated labels are announced as just "edit" — users don't know what to type.

Bad
HTML — Don't do this
<input type="email" placeholder="Enter your email" />
NVDA announces
"edit"
VoiceOver announces
"edit text"

Placeholder text is not a substitute for a label. Most screen readers don't announce placeholder text as the accessible name. The input is effectively unlabeled.

Fixed
HTML — Do this instead
<label for="email">Email address</label>
<input type="email" id="email"
  placeholder="name@company.com" />
NVDA now announces
"Email address, edit"
VoiceOver now announces
"Email address, edit text"

Div used as a button

Using a div with an onClick handler instead of a button element loses keyboard support, focus management, and role announcement.

Bad
HTML — Don't do this
<div class="btn" onclick="handleClick()">
  Submit
</div>
NVDA announces
"Submit"
VoiceOver announces
"Submit"

No "button" role is announced. The element isn't focusable via Tab. Keyboard users can't activate it with Enter or Space. Screen reader users don't know it's interactive.

Fixed
HTML — Do this instead
<button type="submit">
  Submit
</button>
NVDA now announces
"Submit, button"
VoiceOver now announces
"Submit, button"

Link with no text

Links wrapping only an icon or image with no accessible name are announced as just "link" — users can't tell where it goes.

Bad
HTML — Don't do this
<a href="/settings">
  <svg viewBox="0 0 24 24"><!-- gear icon --></svg>
</a>
NVDA announces
"link"
VoiceOver announces
"link"

The link has no accessible name. Screen reader users hear "link" with no destination or purpose. This is especially common in icon-based navigation.

Fixed
HTML — Do this instead
<a href="/settings" aria-label="Settings">
  <svg viewBox="0 0 24 24" aria-hidden="true"><!-- gear icon --></svg>
</a>
NVDA now announces
"Settings, link"
VoiceOver now announces
"Settings, link"

Redundant aria-label on a link

Adding aria-label that duplicates the visible text creates a confusing double announcement or hides the visible text from screen readers.

Bad
HTML — Don't do this
<a href="/pricing" aria-label="Click here to see pricing">
  See Pricing
</a>
NVDA announces
"Click here to see pricing, link"
VoiceOver announces
"Click here to see pricing, link"

The aria-label overrides the visible text. Sighted users see "See Pricing" but screen reader users hear "Click here to see pricing" — a mismatch that breaks the shared experience. Also, "click here" is not descriptive.

Fixed
HTML — Do this instead
<a href="/pricing">
  See Pricing
</a>
NVDA now announces
"See Pricing, link"
VoiceOver now announces
"See Pricing, link"

Multiple h1 elements

Having more than one h1 on a page confuses the document structure. Screen reader users expect a single h1 as the page title.

Bad
HTML — Don't do this
<h1>Speakable</h1>
<main>
  <h1>Documentation</h1>
  <p>Welcome to the docs.</p>
</main>
NVDA announces
"Speakable, heading level 1
Documentation, heading level 1"
VoiceOver announces
"heading level 1, Speakable
heading level 1, Documentation"

Two h1 elements suggest two separate pages or documents. Users navigating by heading can't determine which is the actual page title.

Fixed
HTML — Do this instead
<header>
  <span class="logo">Speakable</span>
</header>
<main>
  <h1>Documentation</h1>
  <p>Welcome to the docs.</p>
</main>
NVDA now announces
"Documentation, heading level 1"
VoiceOver now announces
"heading level 1, Documentation"

Disabled button without accessible state

Using CSS to visually disable a button without the disabled attribute or aria-disabled means screen readers don't know it's inactive.

Bad
HTML — Don't do this
<button class="opacity-50 cursor-not-allowed">
  Submit
</button>
NVDA announces
"Submit, button"
VoiceOver announces
"Submit, button"

The button looks disabled visually but screen readers announce it as a normal, active button. Users will try to activate it and nothing will happen — with no explanation why.

Fixed
HTML — Do this instead
<button disabled>
  Submit
</button>
NVDA now announces
"Submit, button, unavailable"
VoiceOver now announces
"Submit, button, dimmed"

Catch these automatically

Run speakable page.html -f audit to detect many of these issues automatically. The audit report flags missing names, heading hierarchy violations, unnamed landmarks, and more — with specific remediation suggestions for each.