Accessibility Bugs I Keep Finding in Web Apps
The most frequent accessibility violations I encounter in code reviews, why they matter, and the specific fixes.

Try navigating your own website without a mouse. Just the keyboard. Tab through the page. Can you reach every button? Every link? Can you open that dropdown menu? Close the modal?
A friend who uses a screen reader showed me how he browses the web. That demonstration changed how I think about every piece of HTML I write. Not because the technology was exotic โ it wasn't. Basic screen reader navigation on popular websites. What shocked me was how broken everything was. Buttons that didn't announce themselves as buttons. Forms where labels and inputs weren't connected. Modals that trapped focus in the wrong place, or didn't trap it at all.
Since then I've been flagging accessibility issues in code reviews, and the same bugs keep appearing. Each one seems fixable โ usually in a few lines. The impact on people using assistive technology is disproportionate to the effort required.
Caveat: I'm not an accessibility expert. I'm a web developer who's been paying attention to this for a couple of years. Corrections from people who do this professionally would be welcome.
Divs Pretending to Be Buttons
Probably the most common accessibility bug I encounter. Developer needs a clickable element, uses a <div> or <span> with an onClick, and it works for mouse users.
<div class="action-btn" onclick="handleDelete()">Delete Account</div>
For keyboard users, this element doesn't exist. Can't tab to it. Can't activate with Enter or Space. Not in the focus order. Screen reader users hear "Delete Account" as plain text โ no indication it's interactive.
A <button> gives you all of that for free. Keyboard focus. Enter/Space activation. Screen reader announces "button." No JavaScript needed for any of those behaviors.
<button class="action-btn" onclick="handleDelete()">Delete Account</button>
The objection is always "buttons are hard to style." They're not. Reset the defaults:
.action-btn {
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
}
Five lines and you have a blank canvas. Using a utility framework like Tailwind makes it even easier โ I compared the Tailwind vs CSS Modules approach and either gives you full control over button appearance without sacrificing semantics. No good reason to use a div for interactive elements.
Sometimes I see developers add role="button" and tabindex="0" to a div instead of using the native element. Technically makes it accessible, but requires more code (also need keyboard event handlers for Enter and Space), is more fragile, and solves a problem that doesn't need to exist.
Related: <a> tags without an href aren't focusable by default. An <a> with href="#" scrolls to the page top. If the element performs an action rather than navigating, use <button>. If it navigates, use <a> with a real URL. Screen readers announce links and buttons differently, and users expect different behavior from each.
Missing or Useless Alt Text
Two versions of this problem. Either the alt attribute is absent, or it's present but communicates nothing.
<img src="revenue-chart-q4.png" />
<img src="logo.png" alt="image of a logo" />
Missing alt: screen reader reads the filename aloud. User hears "revenue hyphen chart hyphen q four dot P N G." Communicates nothing about the chart data. When alt says "image of a logo" โ redundant. The screen reader already announces it as an image. "Image. Image of a logo." What brand? What does it represent?
Good alt text describes what the image communicates in context:
<img src="revenue-chart.png" alt="Bar chart showing 40% revenue increase in Q4 2025 compared to Q3" />
Decorative images โ backgrounds, dividers, purely visual elements that don't carry information โ get empty alt:
<img src="decorative-wave.svg" alt="" />
Empty alt="" is intentional and different from missing alt. Missing means the screen reader tries to announce something (usually the filename). Empty means "skip this image โ it doesn't carry information." Both are valid in different contexts. The mistake is not considering which applies.
Writing good alt text is harder than it seems. Context-dependent. A photo of a person in a news article needs alt text describing who it is and what they're doing. The same photo as a profile avatar in a chat app might just need the person's name. A chart needs the data trend summarized, not just "a chart." No single rule covers every case.
For complex images like data visualizations, short alt text may not suffice. Consider aria-describedby pointing to a longer description elsewhere on the page, or a data table alternative.
Form Inputs Without Labels
Placeholder text as the only label for a form field is a usability problem for everyone and an accessibility problem specifically.
<input type="email" placeholder="Enter your email address" />
User starts typing, placeholder disappears. Context for what the field is โ gone. Affects sighted users with cognitive disabilities, people with short-term memory issues, and anyone who gets distracted and returns to a half-filled form.
For screen reader users, the situation is worse. Some screen readers read placeholder text as the accessible name. Support isn't consistent. A proper <label> is universally supported and has a useful side effect: clicking the label focuses the associated input, making the click target bigger. For users with motor impairments who struggle to click precisely on a small input box, that expanded target matters.
<label for="email_input">Email Address</label>
<input type="email" id="email_input" placeholder="name@example.com" />
The for attribute on the label matches the id on the input. That's the programmatic association. Without it, the label is just text near the input โ visually grouped but not linked in a way assistive technology understands.
Visually hidden labels for designs where a visible label doesn't fit:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
Hides from sighted users. Remains available to screen readers. A workaround for visual design constraints. Visible labels are almost always preferable โ better for everyone โ but sometimes the design doesn't accommodate them.
Color Contrast
The accessibility requirement that generates the most pushback from designers. WCAG 2.1 requires 4.5:1 contrast ratio for normal text, 3:1 for large text. A lot of modern designs โ light grays on white, muted pastels, thin gray text on slightly less gray backgrounds โ fall short.
The pushback is usually aesthetic, from what I've seen. "That darker gray doesn't match our design language." Understandable concern. But if someone with low vision, or someone viewing their laptop in sunlight, can't read the text, the design achievement is meaningless to them.
Don't always win this argument. Sometimes designs ship with insufficient contrast and the fix gets deprioritized. But showing designers what their page looks like through a contrast simulation tool usually advances the conversation. Chrome DevTools has a contrast checker built into the color picker โ select an element, click the color swatch in Styles, contrast ratio appears with WCAG pass/fail. Takes five seconds.
Accessibility as a spectrum, not a binary. Perfect WCAG compliance is the goal, but even partial improvements help. Can't get the designer from 4.2:1 to 4.5:1? Getting to 4.4:1 still improves the experience for people with low vision.
Focus Management in Modals
Most complex fix on this list. Most commonly forgotten.
When a modal dialog opens, three things need to happen for keyboard and screen reader users:
Focus moves into the modal. Otherwise, pressing Tab cycles through background elements the user can't see, with no way to reach the modal's controls.
Focus gets trapped inside the modal. Tab cycles between the modal's interactive elements โ close button, form fields, action buttons โ and doesn't escape to the page behind.
Background content gets hidden from screen readers. aria-hidden="true" on the main page wrapper prevents screen reader users from navigating to content behind the modal.
When the modal closes, focus returns to the element that triggered it. User clicked "Edit Profile" to open the modal โ closing it should put focus back on "Edit Profile." Without this, focus jumps to the top of the page and the user loses their place.
More implementation work than the other fixes. In React, libraries like react-focus-lock, @radix-ui/react-dialog, or the native <dialog> element (improved browser support recently) handle most of this. <dialog> with showModal() provides focus trapping and background inertness built in.
Building it from scratch involves listening for Tab presses, checking if focus is on the first or last focusable element, wrapping accordingly. Edge cases around dynamically visible elements, nested modals, Escape-to-close interactions with focus restoration. Wrote the manual version once. Probably fiddly for most people. Would recommend a library unless there's a specific reason not to use one.
Heading Structure
Less visible than the others but matters for screen reader navigation. Screen reader users often navigate by jumping between headings โ the equivalent of visually scanning for section titles. Broken heading hierarchy (jumping from <h1> to <h4>, or using headings for styling instead of structure) breaks that navigation.
Seen codebases where <h3> was used because the designer wanted a specific font size, not because the content was a third-level heading. Mixing presentation with semantics. Use CSS to style text at whatever size you want. Use heading levels to reflect document structure.
A page should have one <h1>. Subsequent headings follow in order without skipping levels. <h1> โ <h2> โ <h3> is correct. <h1> โ <h3> skips a level, confuses navigation.
One of the easiest things to get right. One of the most commonly gotten wrong. Probably because most developers don't think of headings as carrying semantic meaning โ they think of them as "big text" and "medium text."
The Pattern Underneath
Most accessibility issues come from using HTML for the wrong purpose. <button> for interactive elements. <a> for navigation. <label> for form labels. <nav> for navigation regions. <main> for main content. <h1> through <h6> for document structure.
Semantic HTML means browsers and assistive technologies do enormous amounts of work for you โ keyboard handling, role announcements, focus management, screen reader navigation. All free. ARIA attributes exist for cases where native HTML falls short (custom widgets, complex interactions), but the first rule of ARIA is literally "don't use ARIA if you can use a native HTML element instead."
Modern CSS layout features like logical properties and container queries help too โ logical properties handle RTL languages automatically, container queries make responsive components more reliable across contexts.
Skip Links
A quick win that most sites miss. Keyboard users navigate by tabbing through interactive elements in order. On a typical page with a navigation bar containing 8-10 links, a keyboard user has to tab through every nav link before reaching the main content. Every single page load. That's a lot of tabbing.
A skip link solves this. It's a link at the very top of the page โ usually visually hidden until it receives focus โ that jumps directly to the main content area:
<a href="#main-content" class="visually-hidden focus:not-sr-only">
Skip to main content
</a>
<!-- ... navigation ... -->
<main id="main-content">
<!-- page content -->
</main>
When the focus:not-sr-only class (or equivalent CSS) kicks in, the link becomes visible only when a keyboard user tabs to it. Sighted mouse users never see it. Keyboard users press Tab once, see the skip link, press Enter, and they're at the content. Simple. Fast. The kind of feature that takes five minutes to implement and dramatically improves the experience for people who navigate by keyboard.
ARIA Live Regions
For dynamic content that updates without a page reload โ notification counts, form validation messages, chat messages, live search results โ sighted users see the change happen. Screen reader users don't, unless you tell the screen reader to announce the update.
ARIA live regions do this:
<div aria-live="polite" aria-atomic="true">
3 items in your cart
</div>
When the text content of this div changes (say, from "3 items" to "4 items"), the screen reader announces the new content. aria-live="polite" means it waits until the user isn't in the middle of something before announcing. aria-live="assertive" interrupts immediately โ appropriate for error messages and alerts, too aggressive for routine updates.
aria-atomic="true" means the entire content of the region is announced, not just the part that changed. Without it, the screen reader might announce just "4" instead of "4 items in your cart," which is less useful without context.
Common mistake: adding aria-live to an element that already has content when the page loads. The screen reader will announce that content immediately, which can be confusing. Add aria-live to an empty container and then populate it, or add it to a container whose content will change. Another gotcha: putting too many live regions on a single page. Each one competes for the screen reader's attention. Had a dashboard once with five live regions updating independently โ screen reader was basically narrating a horse race. Narrowed it down to one primary region and it made far more sense.
This is one of those areas where testing with an actual screen reader (VoiceOver on Mac, NVDA on Windows โ both free) is worth the twenty minutes it takes to learn the basics. The behavior of live regions is easier to understand by hearing it than by reading about it.
Testing Tools
A few tools that help catch accessibility issues during development rather than after shipping:
axe DevTools โ browser extension that scans the current page and reports accessibility violations. Catches about 30-40% of issues automatically (the mechanical ones โ missing alt text, insufficient contrast, missing form labels). Can't catch things that require human judgment (whether alt text is descriptive enough, whether the tab order makes sense).
Lighthouse accessibility audit โ built into Chrome DevTools. Gives a score and a list of issues. Good for a quick overview. Not as detailed as axe for individual component analysis.
eslint-plugin-jsx-a11y โ catches accessibility issues in React code at lint time. Flags missing alt attributes, invalid ARIA usage, non-interactive elements with click handlers. Runs in your editor, catches things before they're committed. Not perfect โ it can't evaluate runtime-generated content โ but it catches the easy mistakes.
None of these replace manual testing with a screen reader and keyboard. Automated tools miss the experiential issues โ "this page is technically accessible but confusing to navigate" isn't something a scanner catches. But running them takes minimal effort and catches the low-hanging fruit.
The Ongoing Learning
Still learning. Areas not covered here: motion sensitivity and prefers-reduced-motion, touch target sizing on mobile, cognitive load considerations, how internationalization interacts with accessibility. Bigger field than expected when I started paying attention. Not confident I'm getting everything right even with the topics covered here.
Modern CSS layout features like logical properties and container queries help too โ logical properties handle RTL languages automatically, container queries make responsive components more reliable across contexts.
But starting with the baseline โ semantic HTML, proper labels, sufficient contrast, keyboard navigation, focus management โ gets you further than you'd expect. Most of the issues I flag in code reviews are violations of these basics, not advanced ARIA patterns. Get the fundamentals right and you're ahead of the vast majority of websites on the internet.
Written by
Anurag Sinha
Full-stack developer specializing in React, Next.js, cloud infrastructure, and AI. Writing about web development, DevOps, and the tools I actually use in production.
Stay Updated
New articles and tutorials sent to your inbox. No spam, no fluff, unsubscribe whenever.
I send one email per week, max. Usually less.
Comments
Loading comments...
Related Articles

Building Production-Ready Apps With Next.js: The Architecture Shift
Tracing the migration path from traditional React SPAs to the Next.js App Router, addressing routing mechanics, caching layers, and server action boundaries.

WebAssembly Demystified โ It's Not Just 'Fast JavaScript'
What WebAssembly actually is under the hood, why calling it fast JavaScript misses the point, and the Rust-to-WASM pipeline I use in real projects.

HTTP/3 and QUIC โ Why HTTP/2 Wasn't the Final Answer
The protocol running a third of the web that most developers haven't thought about. Connection migration, 0-RTT handshakes, and why switching from TCP to UDP was the only way forward.