npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@a11y-pulse/focus-not-obscured-audit

v0.2.0

Published

WCAG 2.4.11 Focus Not Obscured (Minimum) audit that tabs through a page and detects focused elements entirely hidden behind other content.

Readme

@a11y-pulse/focus-not-obscured-audit

npm version CI License: PolyForm Shield 1.0.0

An accessibility audit that aims to verify compliance with WCAG 2.2: 2.4.11 Focus Not Obscured (Minimum). It tabs through a page's focusable elements and detects whether each one is entirely hidden behind other content (a sticky header/footer, a cookie banner, a modal overlay, etc.) while focused. It is built to be framework-agnostic and can be used in any environment that allows you to programmatically focus elements and hit-test the page, such as Puppeteer, Playwright, or Selenium.

This audit was developed by A11y Pulse for its accessibility monitoring service. It is released as source-available under the PolyForm Shield License 1.0.0.

Install

npm install @a11y-pulse/focus-not-obscured-audit @a11y-pulse/tab-orchestrator puppeteer

@a11y-pulse/tab-orchestrator drives the page (tabbing, markers, obscuring measurement) and ships the bundled Puppeteer adaptor; puppeteer itself is only required if you use that adaptor. Other frameworks can supply their own adaptor without installing Puppeteer at all.

Quickstart

import { runFocusNotObscuredAudit } from "@a11y-pulse/focus-not-obscured-audit";
import { PuppeteerAdaptor } from "@a11y-pulse/tab-orchestrator/puppeteer";
import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://who.likesdogs.nz/");

const result = await runFocusNotObscuredAudit(new PuppeteerAdaptor(page), {
  elementLimit: 20,
});

console.log(result.summary);
// { checked: 2, passed: 1, failed: 1, reachedLimit: false, reachedFailedElementLimit: false, timedOut: false }

console.log(result.elements);
// [
//   {
//     selector: 'html>body>a#under-footer',
//     html: '<a id="under-footer" href="#under">',
//     tabIndex: 1,
//     bucket: 'violation',
//     measurement: {
//       coveredFraction: 1,
//       fullyObscured: true,
//       offscreen: false,
//       opacity: 'opaque',
//       obscuredBy: { selector: '#sticky-footer', html: '<div id="sticky-footer">' }
//     }
//   },
// ]

await browser.close();

See @a11y-pulse/focus-appearance-audit's examples/puppeteer for a runnable example of the same shared-adaptor pattern.

Shared tab session

runFocusNotObscuredAudit is a convenience wrapper: it builds a private @a11y-pulse/tab-orchestrator session, attaches one consumer, runs it, and hands back that consumer's result. If you're running more than one tab-driven audit against the same page (for example alongside @a11y-pulse/focus-appearance-audit), drive a single shared orchestrator instead so the page is only tabbed through once. Use createFocusNotObscuredAudit to get a TabConsumer you can attach() yourself:

import { createTabOrchestrator } from "@a11y-pulse/tab-orchestrator";
import { createFocusNotObscuredAudit } from "@a11y-pulse/focus-not-obscured-audit";
import { PuppeteerAdaptor } from "@a11y-pulse/tab-orchestrator/puppeteer";

const orchestrator = createTabOrchestrator(new PuppeteerAdaptor(page));

const notObscured = createFocusNotObscuredAudit({ elementLimit: 50 });
orchestrator.attach(notObscured);

await orchestrator.run();

console.log(notObscured.result);

notObscured.result is only complete once notObscured has disconnected (by hitting one of its own limits) or the session has ended — reading it before then is undefined. See @a11y-pulse/tab-orchestrator for the full session lifecycle and capability model. This audit declares only the "obscuring" capability — it does not need unfocusedPair or baselineStyles, so it can run alongside focus-appearance-audit on the same orchestrator without either one paying for the other's measurements.

Options

The following options can be passed to runFocusNotObscuredAudit as FocusNotObscuredOptions:

| Option | Type | Default | Description | | ------------------------ | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | elementLimit | number | 1024 | Max focusable elements to tab through. | | screenshotSettleDelay | number | 33 | How long to wait (in ms) after each Tab for the page to settle before measuring. | | failedElementLimit | number | 0 (never) | Finish the audit early once this many elements have failed, leaving the rest unchecked. Useful as a fail-fast signal when you only need to know a page has focus problems, not their full extent. | | timeout | number | 0 (no timeout) | Limit how long (in ms) the audit runs before returning the results it has gathered so far. |

Result shape

runFocusNotObscuredAudit resolves to a FocusNotObscuredResult:

type FocusNotObscuredResult = {
  /** Every focusable element that was checked, in tab order. */
  elements: Array<{
    selector: string;
    html: string;
    tabIndex: number;
    /** The AA bucket for this element (see below). */
    bucket: "violation" | "incomplete" | "pass";
    /** The raw obscuring measurement behind `bucket`. */
    measurement: {
      coveredFraction: number;
      fullyObscured: boolean;
      offscreen: boolean;
      opacity: "opaque" | "semi-transparent" | "unknown";
      obscuredBy: { selector: string; html: string } | null;
    };
  }>;
  summary: {
    checked: number;
    passed: number;
    /** Count of elements whose `bucket` is `"violation"`. */
    failed: number;
    /** True if `elementLimit` was hit before tabbing finished. */
    reachedLimit: boolean;
    /** True if the audit stopped early after hitting `failedElementLimit`. */
    reachedFailedElementLimit: boolean;
    /** True if the audit returned early because `timeout` elapsed. */
    timedOut: boolean;
  };
};

Buckets

2.4.11 Focus Not Obscured (Minimum) is a Level AA criterion: when an element receives keyboard focus, it must not be entirely hidden by author-created content (partial coverage is allowed at this level — full protection from any coverage is 2.4.12 Focus Not Obscured (Enhanced), a Level AAA criterion this audit does not check). Each element is placed in one of three buckets:

  • violation — the element is entirely hidden behind opaque content while focused. Fails 2.4.11.
  • incomplete — the audit could not confirm compliance either way: the element was scrolled fully offscreen when focused, or it's entirely covered by something of unknown opacity (rare; usually SVG/canvas-painted overlays the audit can't classify).
  • pass — the element is not obscured at all, only partially obscured, or entirely covered by a semi-transparent overlay (still visible through the cover, so not "entirely hidden").

summary.failed counts only "violation" elements; "incomplete" elements count toward summary.passed since they are not confirmed failures.

Adaptors

The audit itself is framework-agnostic: it drives a page through an adaptor, a small interface of primitives (evaluate JS in the page, press Tab, hit-test the focused element, etc.) that the audit calls without knowing which browser automation library is behind it.

The BrowserAdaptor interface lives in @a11y-pulse/tab-orchestrator, which also ships the bundled PuppeteerAdaptor, backed by a Puppeteer Page. This package re-exports the type so runFocusNotObscuredAudit's argument type is available without a separate import. Other environments (Playwright, Selenium, WebDriver) can be supported by implementing the same interface.

Use @a11y-pulse/tab-orchestrator's src/adaptors/puppeteer.ts as a reference implementation.

Limitations

  • Tab order only. The audit tabs through elements in native tab order. It does not yet exercise arrow-key composite widgets (menus, comboboxes, toolbars, etc.) where focus moves via aria-activedescendant or roving tabindex instead of native Tab.
  • Minimum, not Enhanced. This audit checks 2.4.11 only. It does not check 2.4.12 (AAA), which requires that no part of the focused element be obscured, including by the user's own assistive technology.
  • Opaque hit-testing. Coverage is measured by sampling the focused element's box for unrelated content on top of it; unusual stacking/clip-path/mask combinations could in rare cases confuse the opacity classification, which is why those cases are routed to "incomplete" rather than guessed.

Releasing

Releases are managed in the A11y-Pulse/audits monorepo with Changesets. Publishing uses npm trusted publishing (OIDC). There is no long-lived NPM_TOKEN.

Ship a change

  1. Open a PR against main that includes a changeset (npx changeset) naming @a11y-pulse/focus-not-obscured-audit.
  2. After merge, the Release workflow opens a Version PR. Merging that PR publishes this package to npm and tags @a11y-pulse/focus-not-obscured-audit@<version>.

Trusted Publisher on npm must stay configured for:

| Field | Value | | --- | --- | | Organization or user | A11y-Pulse | | Repository | audits | | Workflow filename | release.yml |

Consumers (e.g. the A11y Pulse runner)

Bumping the published version in downstream apps is a separate change. Update the dependency range / lockfile there after the npm release lands.

License

Released under the PolyForm Shield License 1.0.0, in plain language:

  • Source-available. The source is public and you can read, fork, and modify it.
  • Permitted for non-competing use. You can use this package freely in your own products and services, as long as they don't compete with A11y Pulse.
  • Competing products are forbidden. You may not use this software (or a modified version of it) to build a product or service that competes with A11y Pulse's accessibility monitoring offering.

See LICENSE.md for the full, binding terms.