Fixing Blog Accessibility Bugs You Didn’t Know You Shipped

Fixing Blog Accessibility Bugs You Didn’t Know You Shipped

Screen Readers Hitting a Wall on Skippable Blocks

If you’re using ARIA landmarks or skip-to-content links assuming it Just Works™, brace yourself. I ran into an issue where VoiceOver on macOS wouldn’t let users bypass a sticky header because it declared itself as a <nav role="navigation"> but wasn’t the first <nav> in the DOM. Guess what? Most screen readers will pick the first DOM element with that role—not the one you think is visually first.

The fix? Move your skip link ABOVE any visually hidden but DOM-present nav wrappers. Also, make sure only one thing is using role="navigation" unless you’re letting screen readers list landmarks. Otherwise, it’s like giving two different doors the same room number.

This one threw me for hours because test mode in NVDA seemed fine, but real-world users had no clue the main content was even there.

High-Contrast Mode Wrecking Your Button Styling

Some folks forget that Windows High Contrast actively overrides colors with system-defined logic. Which makes your hover states, fancy gradients, or subtle box shadows utterly useless there. That all gray-on-gray minimalist theme you designed? Turns into unreadable white-on-white.

You’ll have elements that are technically visible, but about as perceptible as an ice cube on an ice rink. If you’re forcing borders off or removing outlines with outline: none without replacing them responsibly in user stylesheets, you’re breaking it for keyboard and HCM users alike.

Undocumented issue worth noting: Setting border: none inside a focus selector still removes the Windows system focus ring, even when High Contrast mode is engaged. It’s never in the docs, but CSS specificity sometimes wins even against OS-level behavior.

Chaos With Tab Orders and Dynamically Injected HTML

One fun evening: I tried using Vue to lazily inject a newsletter signup modal 3 seconds after page load. Works great visually. Problem is, the modal’s close button became tab-focusable BEFORE the modal itself ever became visible. You haven’t lived until your tab key takes you to a ghost element you can’t see and can’t escape.

Browsers don’t delay focusability when new interactive DOM nodes are injected. They’re immediately part of the keyboard nav path unless explicitly set to tabindex="-1" or hidden. If you’re creating or toggling visibility without controlling the focus flow manually, you’re just hoping people don’t notice.

The smarter move is to only add the modal to the DOM when it’s active, then run modalElement.focus() manually. But yeah, you’ll need to juggle autofocus conflicts and z-index hell while you’re at it.

Never Trust Placeholder Text as a Label

Fool me once: I had an AdSense rejection I couldn’t figure out, and after digging through older versions of their feedback (via cached copies), it came down to unlabeled form inputs. I’d been using placeholder="Your Email" instead of real <label> tags. Just because it looks obvious to us doesn’t mean it’s announced properly.

Most screen readers don’t treat placeholders as labels. Mobile ones might hint at it, but there’s zero consistency. Also, if the placeholder vanishes on focus (which it often does), you’re now presenting a form field with zero semantic meaning.

Fix was easy: link a real label using for="email" and stop being lazy. I also learned the hard way that floating labels break voice navigation unless your aria-labeling is incredibly precise. And guess what? It rarely is out of the box.

Focus Rings Getting Nuked by Reset Stylesheets

One recurring accessibility bug I see is when designers or devs start with Normalize or some other CSS reset and end up destroying visible focus indicators without realizing it.

“Why does the sign-up button work with a mouse but vanish when I keyboard tab to it?” – said no sighted QA tester ever.

If you’ve got *:focus { outline: none; } somewhere in your base styles, you’ve already lost. Worse is pairing it with a JavaScript click handler that creates a synthetic focus state on non-interactive divs. It ends up being announced as a focusable region but doesn’t respond to keyboard input. Just use button or a if that’s what it functionally is. Stop pretending divs are buttons just because you want symmetrical border radius behavior.

Mismatch Between Visible State and ARIA State

ARIA is not smart enough to reflect your visual styling decisions. If you’ve got an accordion that expands via some .active class but your aria-expanded attribute is stuck on “false”? Yeah, AT users are just getting pranked. They’re being told a panel is closed while it’s visible on screen.

This becomes wildly common in Angular or React components where devs toggle visibility but forget the corresponding ARIA update. Ironically, CSS animations make this worse, since a panel might be visible for a second but semantically incorrect. Even worse, I once had an AdSense script running in the same component slow down state propagation and leave a stale aria-hidden="true" on a visibly open section.

Some ridiculous but useful quick fixes:

  • Patch ARIA states inside your JavaScript show/hide logic directly.
  • Use MutationObserver to self-audit dynamic region states.
  • Use devtools’ Accessibility tab in Chromium browsers—not just console logs.
  • Keep a manual test checklist for focus state, role labeling, and ARIA toggles.
  • Only animate display: block/none via transforms—not display toggles—when possible.

Dark Mode Inverting Icons in Weird Ways

I thought I nailed a beautiful dark theme. Until I noticed that a few inline SVG icons just disappeared entirely when the OS-level color scheme changed. Turns out, I had fill="currentColor" on them, and forgot that background and text color were being toggled independently in dark mode.

The underlying logic bug: if you use inheriting colors inside embed-style or inline SVGs, they can pick up unintended styles when wrapped inside a class-swapped container. There’s no warning or linting to catch that either.

The worst was with AdSense-supplied ad regions. They dynamically inject small image-based placeholders that don’t obey your dark mode frameworks. I ended up wrapping every ad slot in a forced light-mode override via CSS prefers-color-scheme: light scoped block. Ugly, but saved the readability issue at night.

WAI-ARIA Roles on Non-Interactive Elements

ARIA roles sound like a magic wand. Until you slap role="button" on a <span> without any keyboard listeners. Then congrats—you’ve got an element that screen readers now announce as a button, but pressing Enter or Space does literally nothing.

Worse: Sighted keyboard users can tab to it and get stuck. I’ve sat through a call where a client was hammering the keyboard saying “I’m pressing Enter, why won’t it open?”

Thing is, devs add the ARIA first, then forget the event handlers. Or the CSS team swaps out a button tag for a div because “it was easier to style.” If you absolutely must simulate a role, you’ve got to manually assign tabindex, keydown handlers, AND visible focus treatment. And still, it’s way more reliable to use built-ins and style around them.

Invisible CAPTCHA Prompts Ruining Form Flow

Had a contact form that worked fine in Chrome, tabbed beautifully, even had a nice focus ring. But Safari + VoiceOver users couldn’t submit it. Took forever to figure out that the reCAPTCHA badge had floated into view (visually) but wasn’t being announced at all. The hidden checkbox was sandwiched behind a layer and blocking tab flow silently.

There’s very little in the wild about this either. The element had z-index priority and wasn’t actually hidden. Just positioned such that Safari’s accessibility tree ignored it.

The not-fun workaround was to delay form rendering until after reCAPTCHA JS loaded and then forcibly reorder DOM positions so tab flow still found the checkbox. Even then, I had to aria-label it manually since the default prompt didn’t show up the same on all OSes.

Similar Posts