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.

I started caring about web accessibility after a conversation with a friend who uses a screen reader. He showed me how he navigates the web and I was genuinely surprised by how broken most websites are. Not edge cases or exotic assistive technologies โ basic screen reader navigation on popular sites. Buttons that don't announce themselves as buttons. Forms where you can't tell which label goes with which input. Modals that trap focus in the wrong place, or don't trap it at all.
Since then I've been paying attention to accessibility in code reviews, and the same issues keep showing up. I wanted to write about the ones I see most often because they're all fixable โ usually with a few lines of code โ and the impact on users who rely on assistive technology is significant.
I should say upfront: I'm not an accessibility expert. I'm a web developer who's been learning about this for the past couple of years. If you work in accessibility professionally and I get something wrong here, I'd genuinely appreciate the correction.
Using Divs As Buttons
This is the most common accessibility bug I find. A developer needs a clickable element, they use a <div> or a <span> with an onClick handler, and it works for mouse users. Ship it.
<div class="action-btn" onclick="handleDelete()">Delete Account</div>
For someone using a keyboard, this element doesn't exist. You can't tab to it. You can't activate it with Enter or Space. It's not in the focus order. For someone using a screen reader, it's announced as plain text โ there's no indication that it's interactive. The screen reader user hears "Delete Account" and has no idea they're supposed to click it.
A <button> element gives you all of this for free. Keyboard focus, Enter/Space activation, screen reader announcement as "button." No JavaScript required for any of those behaviors. The browser handles it.
<button class="action-btn" onclick="handleDelete()">Delete Account</button>
The objection I always hear is "buttons are hard to style." They're really not. Reset the default styles and you have a blank canvas:
.action-btn {
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
}
Five lines of CSS resets and you can make the button look like anything. There's no good reason to use a div for interactive elements in 2026 and I'll keep saying that until I stop seeing it in PRs.
Sometimes I see developers add role="button" and tabindex="0" to a div instead of just using a button. This technically makes it accessible but it's more code, it's more fragile (you also need keyboard event handlers for Enter and Space), and it's solving a problem that doesn't need to exist. Use the native element.
Related: I also see <a> tags used as buttons when there's no URL to navigate to. An anchor tag without an href isn't focusable by default. And an anchor tag with href="#" scrolls to the top of the page. If the element performs an action rather than navigating somewhere, it should be a <button>. If it navigates somewhere, it should be an <a> with a real URL. The distinction matters to screen readers because they announce links and buttons differently, and users expect different behavior from each.
Missing Alt Text on Images
Two versions of this problem show up. Either the alt attribute is missing entirely, or it's present but useless.
<img src="revenue-chart-q4.png" />
<img src="logo.png" alt="image of a logo" />
When alt is missing, screen readers read the filename aloud. Your user hears "revenue hyphen chart hyphen q four dot P N G." That communicates nothing about the data in the chart. When alt says "image of a logo," it's redundant โ the screen reader already announces it as an image before reading the alt text. "Image. Image of a logo" is what the user actually hears. What brand? What does the logo 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" />
For decorative images โ backgrounds, dividers, purely visual elements that don't convey information โ use an empty alt to tell screen readers to skip it:
<img src="decorative-wave.svg" alt="" />
The empty alt="" is intentional and different from a missing alt attribute. Missing means the screen reader tries to announce something (usually the filename). Empty means "this image doesn't carry information, skip it." Both are valid in different contexts. The mistake is forgetting to consider which one applies.
Writing good alt text is harder than people think. It depends on context. A photo of a person in a news article needs alt text describing who it is and what they're doing. The same photo used as a profile avatar in a chat app might just need the person's name. A chart needs alt text that summarizes the data trend, not just "a chart." I don't think there's a single rule that covers every case. You have to think about what information the image is conveying to sighted users and translate that into text.
One thing I've learned recently: for complex images like data visualizations, a short alt text might not be sufficient. Consider using aria-describedby pointing to a longer description elsewhere on the page, or providing a data table alternative. I haven't implemented this in practice yet, so I can't speak to how well it works day-to-day.
Form Inputs Without Labels
Relying on placeholder text as the only label for a form field is a usability problem for everyone and an accessibility problem specifically for screen reader users.
<input type="email" placeholder="Enter your email address" />
When the user starts typing, the placeholder disappears. Now they've lost the context for what the field is. This affects sighted users with cognitive disabilities, users with short-term memory issues, and honestly anyone who gets distracted and comes back to a half-filled form. Which field was this again?
For screen reader users, the situation is worse. Some screen readers read placeholder text as the accessible name, but the support isn't consistent. A proper <label> is universally supported and also has a useful side effect: clicking the label focuses the associated input, which makes the click target bigger. For users with motor impairments who struggle to click precisely on a small input box, that expanded target area matters.
<label for="email_input">Email Address</label>
<input type="email" id="email_input" placeholder="[email protected]" />
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 can understand. Screen readers need that link to announce "Email Address, text input" when the field receives focus.
If you're using a design where the label needs to be visually hidden โ floating label patterns, or designs where a visible label doesn't fit โ you can hide it with CSS while keeping it accessible:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
This hides the element from sighted users but keeps it available to screen readers. It's a workaround for visual design constraints. I'd rather have a visible label in every case โ it's better for everyone โ but I understand that sometimes the design doesn't accommodate it.
Color Contrast
This is the accessibility requirement that generates the most pushback from designers. WCAG 2.1 requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text. A lot of modern designs โ light grays on white backgrounds, muted pastels, thin gray text on slightly less gray backgrounds โ don't meet this.
The pushback is usually aesthetic. "That darker gray doesn't match the design language." I understand the concern. But if someone with low vision, or someone looking at their laptop screen in sunlight, can't read your text, the aesthetic achievement is meaningless to them.
I don't always win this argument. Sometimes the design ships with insufficient contrast and the fix gets deprioritized. But I've found that showing the designer what their page looks like through a contrast simulation tool usually moves the conversation forward. Chrome DevTools has a contrast checker built into the color picker โ select an element, click the color swatch in the Styles panel, and it shows the contrast ratio and whether it passes WCAG. Takes five seconds.
There's a broader point here about accessibility as a spectrum rather than a binary. Perfect WCAG compliance is great, but even partial improvements help. If you can't get the designer to change their gray from 4.2:1 to 4.5:1, getting them to 4.4:1 is still an improvement for users with low vision. Don't let perfect be the enemy of better.
Focus Management in Modals
This is the most complex accessibility fix on this list and the one most commonly forgotten. When a modal dialog opens, three things need to happen for keyboard and screen reader users:
First, focus needs to move into the modal. If focus stays on the button behind the overlay, pressing Tab cycles through background elements the user can't see. They have no way to reach the modal's Close button or any of its controls.
Second, focus needs to be trapped inside the modal while it's open. Tab should cycle between the modal's interactive elements โ the close button, form fields, action buttons โ and not escape to the page behind it.
Third, the background content should be hidden from screen readers. Adding aria-hidden="true" to the main page wrapper prevents screen reader users from navigating to content behind the modal, which would be confusing since they can't interact with it while the overlay is up.
And when the modal closes, focus should return to the element that triggered it. If a user clicked "Edit Profile" to open the modal, closing it should put focus back on "Edit Profile." This is an easy detail to forget but it matters โ without it, focus jumps to the top of the page and the user loses their place in the document.
This is more implementation work than the other fixes. If you're using React, libraries like react-focus-lock, @radix-ui/react-dialog, or the HTML <dialog> element (which has improved browser support) handle most of this for you. The native <dialog> element with showModal() provides focus trapping and background inertness out of the box, which is nice.
If you're building it from scratch, the focus trap logic involves listening for Tab key presses, checking if focus is on the first or last focusable element in the modal, and wrapping accordingly. There are edge cases around dynamically visible elements, nested modals, and the interaction between Escape-to-close and focus restoration. I've written the manual version once and it was fiddly. I'd recommend using a library unless you have a reason not to.
Heading Hierarchy
This one is less visible than the others but it matters for screen reader navigation. Screen reader users often navigate pages by jumping between headings โ it's the equivalent of visually scanning a page for section titles. If your heading hierarchy is broken (jumping from <h1> to <h4>, or using heading tags for styling rather than structure), that navigation breaks.
I've seen codebases where <h3> was used because the designer wanted a specific font size, not because the content was a third-level heading. That's mixing presentation with semantics. Use CSS to style text at whatever size you want, and use heading levels to reflect the actual document structure.
A page should have one <h1> (the main title), and subsequent headings should follow in order without skipping levels. <h1> โ <h2> โ <h3> is correct. <h1> โ <h3> skips a level and confuses the navigation.
This is one of the easiest things to get right and one of the most commonly gotten wrong. I think it's because most developers don't think of HTML headings as having semantic meaning โ they think of them as "big text" and "slightly less big text."
The General Principle
Most accessibility issues come down to using HTML for its intended 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.
If your HTML is semantic โ if you're using the right elements for the right purposes โ browsers and assistive technologies do a huge amount of work for you. You get keyboard handling, role announcements, focus management, and screen reader navigation for free. ARIA attributes exist for cases where native HTML doesn't cover your needs (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."
I'm still learning. There are areas I haven't covered โ motion sensitivity and prefers-reduced-motion, touch target sizes on mobile, cognitive load considerations, internationalization interactions with accessibility. It's a bigger field than I expected when I started paying attention to it, and I'm not confident I'm getting everything right even with the things I did cover. But starting with the basics โ semantic HTML, proper labels, sufficient contrast โ gets you a long way.
Written by
Anurag Sinha
Developer who writes about the stuff I actually use day-to-day. If I got something wrong, let me know.
Found this useful?
Share it with someone who might find it helpful too.
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.
Tailwind vs CSS Modules: What We Ended Up Doing
How our team debated and resolved the Tailwind vs CSS Modules question. We didn't pick just one.
What We Learned Migrating to React Server Components
Notes from migrating our SPA to the Next.js App Router and React Server Components. What improved, what broke, and what surprised us.