DEV Community

Varun Manjunath
Varun Manjunath

Posted on

How I approached accessibility in a single-page PWA — and what I still need help with

I built NoSuggest — a free, open-source Progressive Web App that lets you watch YouTube without the algorithm. No recommendations, no Shorts, no auto-play. Just the channels you explicitly choose.

It started as a personal tool. Then teachers started using it in classrooms. Then it got listed on the ISTE EdTech Index. Suddenly accessibility wasn't optional, it was the right thing to do, and genuinely important for the people using it.

I want to share what I actually did, what was harder than expected, and where I got it wrong the first time. And at the end — an honest ask.


The architecture that made accessibility tricky

NoSuggest is a single HTML file. No framework. No build step. All screens are position:absolute divs that toggle opacity and pointer-events. This meant the usual accessibility patterns didn't apply cleanly — I had to think carefully about what "navigation" even means when there's no page load.

The biggest early mistake: inactive screens were still in the Tab order. You would Tab through the Feed screen and silently end up inside the Settings screen — completely invisible to a sighted user but reachable by keyboard. Classic SPA trap.


What I fixed, and how

1. inert for inactive screens

The fix was the inert attribute — it removes an entire element's subtree from Tab order and screen reader traversal in one shot, with no JavaScript loop needed:

screens.forEach(s => s.setAttribute('inert', ''));
activeScreen.removeAttribute('inert');
Enter fullscreen mode Exit fullscreen mode

Progressive enhancement: browsers without inert support just ignore it and fall back to the previous behaviour. No breakage.


2. Modal focus trapping

Every modal in the app (Export Backup, Kids Mode PIN, iOS install sheet, video player) now traps Tab inside it and returns focus to the triggering element on close. One shared utility handles all of them:

function a11yOpenModal(modalEl, closeFn) {
  _a11yLastFocused = document.activeElement;
  // focus first focusable inside modal
}

function a11yCloseModal() {
  _a11yLastFocused.focus(); // return focus
}
Enter fullscreen mode Exit fullscreen mode

Escape closes any open modal from anywhere — consistent and expected.


3. Skip links

The Feed screen can have 10+ channels, each with 10 videos and a bookmark button — that's potentially 200+ Tab stops before you reach the nav. Two skip link patterns helped:

  • A "Skip to main content" link as the very first focusable element on the page
  • A per-channel skip link on the Feed, hidden until focused, that jumps straight to the next channel — chainable, so you can skip several in a row with Enter, Enter, Enter

Both use the classic off-screen-until-focused pattern:

.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  position: fixed;
  left: 12px;
  top: 12px;
}
Enter fullscreen mode Exit fullscreen mode

4. The YouTube iframe problem

The video player uses the YouTube IFrame API. Once focus enters the iframe, it disappears into a cross-origin document — our Tab trap can't see it, so focus could wander through however many controls YouTube exposes before returning.

Fix: set tabIndex = -1 on the iframe element itself once the player is ready. Our own Close and Save buttons remain in the Tab order; the iframe content is excluded entirely.

onReady: () => {
  const iframeEl = ytPlayer.getIframe();
  if (iframeEl) iframeEl.tabIndex = -1;
}
Enter fullscreen mode Exit fullscreen mode

5. prefers-reduced-motion

One media query, applied globally:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

All screen transitions, modal fades, and the install prompt pulse animation still function — they just happen near-instantly. No vestibular triggers.


6. iOS PWA nav bar gap — an unexpected accessibility-adjacent fix

On iOS in standalone (Add to Home Screen) mode, overflow:hidden on the app shell caused position:fixed children — including the bottom nav — to be clipped relative to the app container rather than the true viewport. The nav appeared to hover above the bottom of the screen with a visible gap.

The fix: overflow:clip doesn't create a new containing block for fixed children, so they anchor to the real viewport correctly.

#app { overflow: hidden; } /* fallback */

@supports (overflow: clip) {
  #app { overflow: clip; }
}
Enter fullscreen mode Exit fullscreen mode

Not strictly a WCAG issue, but a real usability barrier for people using the PWA on iPhone — worth documenting.


7. Programmatic focus outlines

showScreen() moves focus to the active screen's heading for screen reader announcement. But this triggered a visible focus ring on the heading for mouse and touch users too — confusing, since they didn't "Tab" there.

[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible {
  outline: none;
  box-shadow: none;
}
Enter fullscreen mode Exit fullscreen mode

Safe to suppress because tabindex="-1" means the element is never reachable by Tab — only by script. Keyboard users are unaffected.


What's documented

All of this is on nosuggest.com/accessibility — an honest account of what's implemented and what isn't. I tried to write it the way I'd want to read it: specific, no vague compliance claims, and transparent about gaps.


Where I need help

I'm applying for the CAST / Digital Promise "Accessibility Baseline" certification (required for the ISTE EdTech listing to show a verified badge).

The one thing I'm missing: a third-party WCAG 2.1 AA conformance review (ACR/VPAT) completed by someone independent. Self-assessment doesn't satisfy their requirement.

NoSuggest is:

  • A single HTML file (one page, ~8 distinct "screens")
  • No backend, no login, no data collected
  • Completely free and open source — github.com/No-Suggest/NoSuggest

If you are an accessibility professional who does volunteer reviews, or know someone who does, I would genuinely appreciate hearing from you. The contact form is at nosuggest.com/contact.

And if you've tackled similar SPA accessibility challenges — especially with focus management or skip link patterns — I'd love to hear what worked for you in the comments.

Thank you very much!

Top comments (0)