{"_id":"@a11y-pulse/focus-appearance-audit","_rev":"6-451c307e106244992968fe4b32a20f1e","name":"@a11y-pulse/focus-appearance-audit","dist-tags":{"latest":"0.4.0"},"versions":{"0.0.0":{"name":"@a11y-pulse/focus-appearance-audit","version":"0.0.0","license":"UNLICENSED","_id":"@a11y-pulse/focus-appearance-audit@0.0.0","maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"dist":{"shasum":"f527e8e03b8503ce96e2563149b0ef7d1c906c74","tarball":"https://registry.npmjs.org/@a11y-pulse/focus-appearance-audit/-/focus-appearance-audit-0.0.0.tgz","fileCount":2,"integrity":"sha512-7EADyjmMpq8GjjjOzP29q0qlLVPYEE8Kt23ypDyrs/P29E4GGQpw7+nH5bgtdWTmxfGfsnAKd8ZSb0ffDzd93Q==","signatures":[{"sig":"MEYCIQDO5KNbtC+VV2fe7BW8FOODjXZx4KEVWUIf1bVd6n1TgAIhAIN2slFsl+RSjWfz7LVGG8e9yxIG4qEvIreyW7qchH2k","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":269},"private":false,"_npmUser":{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"},"_npmVersion":"11.11.0","description":"Placeholder — real package published from GitHub Actions","directories":{},"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/focus-appearance-audit_0.0.0_1784846847955_0.7458540949515426","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@a11y-pulse/focus-appearance-audit","version":"0.1.0","keywords":["accessibility","a11y","wcag","focus-visible","focus-appearance","puppeteer","audit"],"author":{"name":"A11y Pulse Limited"},"license":"SEE LICENSE IN LICENSE.md","_id":"@a11y-pulse/focus-appearance-audit@0.1.0","maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"homepage":"https://github.com/A11y-Pulse/focus-appearance-audit#readme","bugs":{"url":"https://github.com/A11y-Pulse/focus-appearance-audit/issues"},"dist":{"shasum":"c8b11a39c5288a2d4eb9c8ffdc1d96cb2623b588","tarball":"https://registry.npmjs.org/@a11y-pulse/focus-appearance-audit/-/focus-appearance-audit-0.1.0.tgz","fileCount":10,"integrity":"sha512-T+mdpS4AhKpnZy72Ku0mwz1f/2KSjCaSHkiE0qARxM0+o/voN1yVLhk/dXqtRDibqNQPEsxNDkRZXOrFmj07ZA==","signatures":[{"sig":"MEYCIQDMWwrAVwoM8l56USI0LryhNiFo9QjRykqx1ZMCgzR07AIhAJvZY6OSV79fsor2czzDQY1kjtc5ZARjrWUvVbQ/3Rk5","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@a11y-pulse%2ffocus-appearance-audit@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":112893},"type":"module","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./puppeteer":{"types":"./dist/adaptors/puppeteer.d.ts","import":"./dist/adaptors/puppeteer.js"}},"gitHead":"dca9bb0c20cce517b11432f68764e01cd868a4cb","scripts":{"docs":"typedoc","lint":"biome check","test":"npm run typecheck && npm run test:unit","build":"tsup","release":"npm run build && changeset publish","lint:fix":"biome check --fix","test:unit":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run build","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d455551b-5472-4feb-be13-77a80dad321a"}},"repository":{"url":"git+https://github.com/A11y-Pulse/focus-appearance-audit.git","type":"git"},"_npmVersion":"12.0.1","description":"WCAG 2.4.7 Focus Visible audit that tabs through a page and detects visible focus indicators via computed styles and pixel-diff fallback.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"pngjs":"^7.0.0","pixelmatch":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^4.1.7","typedoc":"^0.28.5","happy-dom":"^20.9.0","puppeteer":"^24.15.0","typescript":"^6.0.2","@types/node":"^22.0.0","@types/pngjs":"^6.0.5","@biomejs/biome":"^2.5.4","@changesets/cli":"^2.29.5","@tsconfig/node22":"^22.0.2","@types/pixelmatch":"^5.2.6"},"peerDependencies":{"puppeteer":"^24.15.0"},"peerDependenciesMeta":{"puppeteer":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/focus-appearance-audit_0.1.0_1784847049164_0.5885398363677445","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@a11y-pulse/focus-appearance-audit","version":"0.2.0","keywords":["accessibility","a11y","wcag","focus-visible","focus-appearance","puppeteer","audit"],"author":{"name":"A11y Pulse Limited"},"license":"SEE LICENSE IN LICENSE.md","_id":"@a11y-pulse/focus-appearance-audit@0.2.0","maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"homepage":"https://github.com/A11y-Pulse/focus-appearance-audit#readme","bugs":{"url":"https://github.com/A11y-Pulse/focus-appearance-audit/issues"},"dist":{"shasum":"f48ddc690b509ba97ffaa908b226a0f3af7d2138","tarball":"https://registry.npmjs.org/@a11y-pulse/focus-appearance-audit/-/focus-appearance-audit-0.2.0.tgz","fileCount":17,"integrity":"sha512-p6Mp40Dn6yxyfMe2pFGmdF8rBr/N/5Lqyqqz2BWiX6MwCfkZ4t1Qh/2lVD+hZsQ9LmoPqHTXlN7gfT5lEvyeAA==","signatures":[{"sig":"MEQCIDPcxWTTZVhcrl1hy2e9kV+zsENQXRPB+Ma8ADrCeJoCAiBQoWMdFWt//lUUCvdzqltT+mYasNZRkC2S25FxtqGTMQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@a11y-pulse%2ffocus-appearance-audit@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":122863},"type":"module","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./puppeteer":{"types":"./dist/adaptors/puppeteer.d.ts","import":"./dist/adaptors/puppeteer.js"}},"gitHead":"df13a287975c1a096dad232885ea046d2b7d3bb8","scripts":{"lint":"biome check","test":"npm run typecheck && npm run test:unit","build":"tsup && tsc -p tsconfig.build.json","lint:fix":"biome check --fix","test:unit":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run build","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d455551b-5472-4feb-be13-77a80dad321a"}},"repository":{"url":"git+https://github.com/A11y-Pulse/focus-appearance-audit.git","type":"git"},"_npmVersion":"12.0.1","description":"WCAG 2.4.7 Focus Visible audit that tabs through a page and detects visible focus indicators via computed styles and pixel-diff fallback.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"pngjs":"^7.0.0","pixelmatch":"^7.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^4.1.7","happy-dom":"^20.9.0","puppeteer":"^25.3.0","typescript":"^7.0.2","@types/node":"^26.1.1","@types/pngjs":"^6.0.5","@biomejs/biome":"^2.5.4","@tsconfig/node22":"^22.0.2"},"peerDependencies":{"puppeteer":"^25.3.0"},"peerDependenciesMeta":{"puppeteer":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/focus-appearance-audit_0.2.0_1784851122878_0.12964178036171292","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@a11y-pulse/focus-appearance-audit","version":"0.3.0","keywords":["accessibility","a11y","wcag","focus-visible","focus-appearance","puppeteer","audit"],"author":{"name":"A11y Pulse Limited"},"license":"SEE LICENSE IN LICENSE.md","_id":"@a11y-pulse/focus-appearance-audit@0.3.0","maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"homepage":"https://github.com/A11y-Pulse/audits/tree/main/packages/focus-appearance-audit#readme","bugs":{"url":"https://github.com/A11y-Pulse/audits/issues"},"dist":{"shasum":"3d22290c141b16e36b131b5b7f3c447bd8ce3a34","tarball":"https://registry.npmjs.org/@a11y-pulse/focus-appearance-audit/-/focus-appearance-audit-0.3.0.tgz","fileCount":10,"integrity":"sha512-C5nGvjWVsb+pBc+wDQGlLOVCmY/EwG5YHKGCZXVH+Kjwc+AJYj2tt6ekT7as/K68VNGVAMHjlL1Sna52ZMlpVg==","signatures":[{"sig":"MEYCIQC9QiG3nf8p/5P3eT6B8osnRuPq3DcTMohE6kWk6dujAgIhAKUKZuMap9jSB4jifl0UHTdroRbgKSCRuBm22NUnKrVI","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@a11y-pulse%2ffocus-appearance-audit@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":58418},"type":"module","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"026c2de7118e1ae6a2b310cabb0bc6bc5368f1e8","scripts":{"lint":"biome check","test":"npm run typecheck && npm run test:unit","build":"tsup && tsc -p tsconfig.build.json","lint:fix":"biome check --fix","test:unit":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run build","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b43b4527-9bbf-4f0c-a5f3-5ebd6e81a442"}},"repository":{"url":"git+https://github.com/A11y-Pulse/audits.git","type":"git","directory":"packages/focus-appearance-audit"},"_npmVersion":"12.0.2","description":"WCAG 2.4.7 Focus Visible audit that tabs through a page and detects visible focus indicators via computed styles and pixel-diff fallback.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"pngjs":"^7.0.0","pixelmatch":"^7.2.0","@a11y-pulse/tab-orchestrator":"*"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"happy-dom":"^20.11.2","puppeteer":"^25.7.0","@types/pngjs":"^6.0.5"},"_npmOperationalInternal":{"tmp":"tmp/focus-appearance-audit_0.3.0_1787214145650_0.6196978794798025","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@a11y-pulse/focus-appearance-audit","version":"0.3.1","keywords":["accessibility","a11y","wcag","focus-visible","focus-appearance","puppeteer","audit"],"author":{"name":"A11y Pulse"},"license":"SEE LICENSE IN LICENSE.md","_id":"@a11y-pulse/focus-appearance-audit@0.3.1","maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"homepage":"https://github.com/A11y-Pulse/audits/tree/main/packages/focus-appearance-audit#readme","bugs":{"url":"https://github.com/A11y-Pulse/audits/issues"},"dist":{"shasum":"670a92eb5f312de6df3da67f05eec51f6f479d5d","tarball":"https://registry.npmjs.org/@a11y-pulse/focus-appearance-audit/-/focus-appearance-audit-0.3.1.tgz","fileCount":10,"integrity":"sha512-IK127tJenAWOW4Vg0pYQpTaNDHAC1fJAeyI8uhQJHCNnx80FgJcXpZtGBvvuHEPewZrT4jOHb/w/G+6x98NxsQ==","signatures":[{"sig":"MEQCIFwdK/qceUE1hoin2XHwdZ+0xNBp7la1vmbmOlwzuEzGAiAT/zp47HUzrLq+r4f0Fk53SoAkEQjpV5LSxn9oE/XXxw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@a11y-pulse%2ffocus-appearance-audit@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":57445},"type":"module","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","source":"./src/index.ts"}},"gitHead":"8431177b6098e09064306dc08bddfb59796b444d","scripts":{"lint":"biome check","test":"npm run typecheck && npm run test:unit","build":"tsup && tsc -p tsconfig.build.json","lint:fix":"biome check --fix","test:unit":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run build","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b43b4527-9bbf-4f0c-a5f3-5ebd6e81a442"}},"repository":{"url":"git+https://github.com/A11y-Pulse/audits.git","type":"git","directory":"packages/focus-appearance-audit"},"_npmVersion":"12.0.2","description":"WCAG 2.4.7 Focus Visible audit that tabs through a page and detects visible focus indicators via computed styles and pixel-diff fallback.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"pngjs":"^7.0.0","pixelmatch":"^7.2.0","@a11y-pulse/tab-orchestrator":"*"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"happy-dom":"^20.12.0","puppeteer":"^25.9.0","@types/pngjs":"^6.0.5"},"_npmOperationalInternal":{"tmp":"tmp/focus-appearance-audit_0.3.1_1788834836166_0.6635364364959386","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@a11y-pulse/focus-appearance-audit","version":"0.4.0","description":"WCAG 2.4.7 Focus Visible audit that tabs through a page and detects visible focus indicators via computed styles and pixel-diff fallback.","type":"module","license":"SEE LICENSE IN LICENSE.md","author":{"name":"A11y Pulse"},"homepage":"https://github.com/A11y-Pulse/audits/tree/main/packages/focus-appearance-audit#readme","repository":{"type":"git","url":"git+https://github.com/A11y-Pulse/audits.git","directory":"packages/focus-appearance-audit"},"bugs":{"url":"https://github.com/A11y-Pulse/audits/issues"},"keywords":["accessibility","a11y","wcag","focus-visible","focus-appearance","puppeteer","audit"],"engines":{"node":">=22"},"exports":{".":{"source":"./src/index.ts","types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsup && tsc -p tsconfig.build.json","typecheck":"tsc --noEmit","lint":"biome check","lint:fix":"biome check --fix","test:unit":"vitest run","test:integration":"vitest run --config vitest.integration.config.ts","test":"npm run typecheck && npm run test:unit","prepublishOnly":"npm run build"},"publishConfig":{"access":"public"},"dependencies":{"@a11y-pulse/browser-adaptor":"*","@a11y-pulse/tab-orchestrator":"*","pixelmatch":"^7.2.0","pngjs":"^7.0.0"},"devDependencies":{"@types/pngjs":"^6.0.5","happy-dom":"^20.12.0","playwright-core":"^1.63.0","puppeteer":"^25.10.0"},"gitHead":"facc34c09fbb3982d29a1dc04944b5d035af7780","_id":"@a11y-pulse/focus-appearance-audit@0.4.0","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-kMEHajeNVTcpXa4dkH3l77H96c/p1WQu1vdpf3KAwuvPvgbKck4HEpc2t/hFWPxJOSFX6R0DRPjW00rzz4RXsQ==","shasum":"dc0f682790ef8200761ce4a8984b99b9579984ee","tarball":"https://registry.npmjs.org/@a11y-pulse/focus-appearance-audit/-/focus-appearance-audit-0.4.0.tgz","fileCount":10,"unpackedSize":56382,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@a11y-pulse%2ffocus-appearance-audit@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICLkJMR0IkHH58FVmsil7L9rZtPPsx+i9iQn7waHjnVdAiEA8+35QjCL1hooCf64SfU5vP0+w/iK4dh2I6xkgpb/NE0="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b43b4527-9bbf-4f0c-a5f3-5ebd6e81a442"}},"directories":{},"maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/focus-appearance-audit_0.4.0_1789286273312_0.7508379921034873"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-23T22:47:27.800Z","modified":"2026-09-13T07:57:53.748Z","0.0.0":"2026-07-23T22:47:28.100Z","0.1.0":"2026-07-23T22:50:49.329Z","0.2.0":"2026-07-23T23:58:43.001Z","0.3.0":"2026-08-20T08:22:25.794Z","0.3.1":"2026-09-08T02:33:56.336Z","0.4.0":"2026-09-13T07:57:53.450Z"},"bugs":{"url":"https://github.com/A11y-Pulse/audits/issues"},"author":{"name":"A11y Pulse"},"license":"SEE LICENSE IN LICENSE.md","homepage":"https://github.com/A11y-Pulse/audits/tree/main/packages/focus-appearance-audit#readme","keywords":["accessibility","a11y","wcag","focus-visible","focus-appearance","puppeteer","audit"],"repository":{"type":"git","url":"git+https://github.com/A11y-Pulse/audits.git","directory":"packages/focus-appearance-audit"},"description":"WCAG 2.4.7 Focus Visible audit that tabs through a page and detects visible focus indicators via computed styles and pixel-diff fallback.","maintainers":[{"name":"wildlyinaccurate","email":"joseph@wildlyinaccurate.com"}],"readme":"# @a11y-pulse/focus-appearance-audit\n\n[![npm version](https://img.shields.io/npm/v/@a11y-pulse/focus-appearance-audit)](https://www.npmjs.com/package/@a11y-pulse/focus-appearance-audit)\n[![CI](https://github.com/A11y-Pulse/audits/actions/workflows/ci.yml/badge.svg)](https://github.com/A11y-Pulse/audits/actions/workflows/ci.yml)\n[![License: PolyForm Shield 1.0.0](https://img.shields.io/badge/license-PolyForm%20Shield%201.0.0-blue)](./LICENSE.md)\n\nAn accessibility audit that aims to verify compliance with [**WCAG 2.0: 2.4.7 Focus Visible**](https://www.w3.org/WAI/WCAG20/Understanding/focus-visible). It tabs through a page's focusable elements and detects whether each one shows a visible focus indicator. It is built to be framework-agnostic and can be used in any environment that allows you to programmatically focus elements and read their computed styles, such as Puppeteer, Playwright, or Selenium.\n\nThis audit was developed by [A11y Pulse](https://www.a11ypulse.com/) for its accessibility monitoring service. It is released as source-available under the [PolyForm Shield License 1.0.0](#license).\n\n## Install\n\n```bash\nnpm install @a11y-pulse/focus-appearance-audit @a11y-pulse/tab-orchestrator @a11y-pulse/browser-adaptor puppeteer\n```\n\n`@a11y-pulse/tab-orchestrator` drives the page (tabbing, markers, screenshots), and [`@a11y-pulse/browser-adaptor`](../browser-adaptor) ships the bundled [Puppeteer and Playwright adaptors](#adaptors); `puppeteer` and `playwright-core` are optional peers of that package, each only required if you use the matching adaptor. Other frameworks can supply their own adaptor without installing either.\n\n## Quickstart\n\n```js\nimport { runFocusAppearanceAudit } from \"@a11y-pulse/focus-appearance-audit\";\nimport { PuppeteerAdaptor } from \"@a11y-pulse/browser-adaptor/puppeteer\";\nimport puppeteer from \"puppeteer\";\n\nconst browser = await puppeteer.launch();\nconst page = await browser.newPage();\nawait page.goto(\"https://who.likesdogs.nz/\");\n\nconst result = await runFocusAppearanceAudit(new PuppeteerAdaptor(page), {\n  elementLimit: 20,\n});\n\nconsole.log(result.summary);\n// { checked: 2, passed: 1, failed: 1, reachedLimit: false, reachedFailedElementLimit: false, timedOut: false }\n\nconsole.log(result.elements);\n// [\n//   {\n//     selector: 'html>body>button',\n//     html: '<button>',\n//     tabIndex: 1,\n//     passed: true,\n//     detectionMethod: 'style'\n//   },\n//   {\n//     selector: 'html>body>section.wrap>article.pb-3>ul>li>a',\n//     html: '<a href=\"https://github.com/wildlyinaccurate/second\">',\n//     tabIndex: 2,\n//     passed: false,\n//     detectionMethod: null\n//   },\n// ]\n\nawait browser.close();\n```\n\nSee [`examples/puppeteer`](./examples/puppeteer) for a complete, runnable example.\n\n## Shared tab session\n\n`runFocusAppearanceAudit` 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, drive a single shared orchestrator instead so the page is only tabbed through once. Use `createFocusAppearanceAudit` to get a `TabConsumer` you can `attach()` yourself:\n\n```ts\nimport { createTabOrchestrator } from \"@a11y-pulse/tab-orchestrator\";\nimport { createFocusAppearanceAudit } from \"@a11y-pulse/focus-appearance-audit\";\nimport { PuppeteerAdaptor } from \"@a11y-pulse/browser-adaptor/puppeteer\";\n\nconst orchestrator = createTabOrchestrator(new PuppeteerAdaptor(page));\n\nconst focus = createFocusAppearanceAudit({ elementLimit: 50 });\norchestrator.attach(focus);\n\nawait orchestrator.run();\n\nconsole.log(focus.result);\n```\n\n`focus.result` is only complete once `focus` 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`](../tab-orchestrator) for the full session lifecycle and capability model.\n\n## Browser support\n\n| Adaptor | Browser | Supported |\n| --- | --- | --- |\n| Puppeteer | Chrome | Yes |\n| Playwright | Chromium | Yes |\n| Playwright | WebKit | **No.** WebKit does not move focus to links when Tab is pressed, so the audit only reaches a subset of the page. |\n| Playwright | Firefox | **Partial.** Elements inside a closed shadow root get no detection, where Chromium reports a pixel diff. Everything else matches. |\n\nVerified by this repo's integration suites, which run every audit against each of these engines. Unsupported and partial cases are skipped there with the reason printed alongside them.\n\n## Options\n\nThe following options can be passed to `runFocusAppearanceAudit` as `FocusAppearanceOptions`:\n\n| Option                    | Type      | Default             | Description                                                                                                                      |\n| ------------------------- | --------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| `elementLimit`            | `number`  | `1024`               | Max focusable elements to tab through.                                                                                           |\n| `baselineElementLimit`    | `number`  | `elementLimit * 2`   | How many focusable elements to snapshot baseline styles for up front. Not all focusable elements are tabbable (e.g. elements inside menus or hidden containers), so this acts as a floor: the effective baseline budget is `max(elementLimit, baselineElementLimit)`. |\n| `screenshotSettleDelay`   | `number`  | `33`                 | How long to wait (in ms) after each Tab for focus styles/transitions to settle.                                                  |\n| `screenshotClipBuffer`    | `number`  | `10`                 | Padding (in px) around the element box for the pixel-diff screenshot.                                                            |\n| `screenshotDiffThreshold` | `number`  | `4`                  | Number of pixels that must differ to consider an indicator present. Floored at `1`.                                              |\n| `skipStyleCheck`          | `boolean` | `false`              | Skip the computed-style stage and run a pixel diff for every element. Much slower, but may reduce false positives in rare cases. |\n| `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. |\n| `timeout`                 | `number`  | `0` (no timeout)     | Limit how long (in ms) the audit runs before returning the results it has gathered so far.                                       |\n\n## Result shape\n\n`runFocusAppearanceAudit` resolves to a `FocusAppearanceResult`:\n\n```ts\ntype FocusAppearanceResult = {\n  /** Every focusable element that was checked, in tab order. */\n  elements: Array<{\n    selector: string;\n    html: string;\n    tabIndex: number;\n    passed: boolean;\n    detectionMethod: \"style\" | \"pixel-diff\" | null;\n  }>;\n  summary: {\n    checked: number;\n    passed: number;\n    failed: number;\n    /** True if `elementLimit` was hit before tabbing finished. */\n    reachedLimit: boolean;\n    /** True if the audit stopped early after hitting `failedElementLimit`. */\n    reachedFailedElementLimit: boolean;\n    /** True if the audit returned early because `timeout` elapsed. */\n    timedOut: boolean;\n  };\n};\n```\n\n## Detection methods\n\nFocus appearance detection runs in either one or two stages. The first stage is a fast and memory efficient comparison of computed styles. If no style changes are detected, a second stage captures screenshots of the focused element and compares them pixel by pixel. The second stage is more expensive but can detect focus appearance that does not show in computed styles.\n\n### Stage 0: Preparation\n\nAt the beginning of the audit, a snapshot of each focusable element's computed styles is taken. This snapshot contains only [focus-relevant properties](./src/focus-style.ts) and the elements' `::before` & `::after` pseudo-elements. The audit then begins a loop of tabbing through each focusable element and running it through the next two stages.\n\n### Stage 1: computed style comparison\n\n![Three steps: a navigation menu with nothing focused, then Tab moving focus to the first link which gains a blue outline, then the link's computed style changing from outline none to a solid blue outline, which is detected as a pass](./docs/detection-style.svg)\n\nOnce an element is focused, its computed styles are captured again and compared to the baseline. If any property has changed, the element passes with `detectionMethod: \"style\"`; if not, it moves to Stage 2. This stage is very fast and has not produced any false positives in testing.\n\n### Stage 2: pixel-diff fallback\n\n![Five steps: the page, then Tab focusing a link styled only with a filter glow, then the watched computed styles looking identical before and after, then falling back to a pixel diff, then a focused versus unfocused versus diff comparison of the clipped link where the diff highlights the changed pixels](./docs/detection-pixel.svg)\n\nSince focus appearance can be achieved without modifying style properties (a `filter`, an animated background), a more expensive pixel-diff fallback is used. In this stage, a clipped screenshot is taken of the element in both its focused and unfocused states. The two images are compared pixel by pixel, and if they differ beyond a certain threshold, the element passes with `detectionMethod: \"pixel-diff\"`.\n\n## Accuracy\n\nThis audit is a heuristic and can be wrong in both directions. See [docs/accuracy.md](./docs/accuracy.md) for the known false positives, false negatives, and intentional failures.\n\n## WCAG 2.0 2.4.7 (Focus Visible) vs WCAG 2.2 2.4.13 (Focus Appearance)\n\nBoth success criteria concern the keyboard focus indicator, but they set very different bars. [2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG20/Understanding/focus-visible) is a Level AA criterion from WCAG 2.0 and is purely existential: when an element receives keyboard focus, some visible focus indicator must appear. It says nothing about how large or how visible that indicator has to be, so even a faint one-pixel outline satisfies it.\n\n[2.4.13 Focus Appearance](https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance) is a Level AAA criterion added in WCAG 2.2 that closes that gap. On top of requiring an indicator, it sets a minimum size (at least the area of a 2 CSS pixel thick perimeter of the component) and a minimum contrast (a ratio of at least 3:1 between the focused and unfocused states of the same pixels).\n\nAt the time of writing this audit only checks for 2.4.7. Experimental support for 2.4.13 will be added in a future release.\n\n## Adaptors\n\nThe audit itself is framework-agnostic: it drives a page through an **adaptor**, a small interface of primitives (evaluate JS in the page, press Tab, take a clipped screenshot, etc.) that the audit calls without knowing which browser automation library is behind it.\n\nThe `BrowserAdaptor` interface itself lives in [`@a11y-pulse/browser-adaptor`](../browser-adaptor), which also ships the bundled `PuppeteerAdaptor` and `PlaywrightAdaptor`. This package re-exports the type (aliased here as `FocusAppearanceAuditAdaptor`) so `runFocusAppearanceAudit`'s argument type is available without a separate import. Other environments (Selenium, WebDriver) can be supported by implementing the same interface.\n\nUse [`@a11y-pulse/browser-adaptor`'s `src/adaptors/puppeteer.ts`](../browser-adaptor/src/adaptors/puppeteer.ts) as a reference implementation.\n\n## Limitations\n\n- **Focus emulation.** Accurate `:focus`/`document.hasFocus()` reporting for a backgrounded page relies on focus emulation. The Puppeteer adaptor enables it over CDP; Playwright enables its own for every page it creates, on all three engines. An adaptor that offers neither will see the page lose focus mid-audit, which ends the session as `lostFocus` rather than producing failures.\n- **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.\n- **Heuristic accuracy.** Detection is heuristic and can produce false positives and false negatives. See [Accuracy](#accuracy) and [docs/accuracy.md](./docs/accuracy.md) for known cases.\n\n## Releasing\n\nReleases are managed in the [A11y-Pulse/audits](https://github.com/A11y-Pulse/audits) monorepo with [Changesets](https://github.com/changesets/changesets). Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC). There is no long-lived `NPM_TOKEN`.\n\n### Ship a change\n\n1. Open a PR against `main` that includes a changeset (`npx changeset`) naming `@a11y-pulse/focus-appearance-audit`.\n2. After merge, the Release workflow opens a Version PR. Merging that PR publishes this package to npm and tags `@a11y-pulse/focus-appearance-audit@<version>`.\n\nTrusted Publisher on npm must stay configured for:\n\n| Field | Value |\n| --- | --- |\n| Organization or user | `A11y-Pulse` |\n| Repository | `audits` |\n| Workflow filename | `release.yml` |\n\n### Consumers (e.g. the A11y Pulse runner)\n\nBumping the published version in downstream apps is a separate change. Update the dependency range / lockfile there after the npm release lands.\n\n## License\n\nReleased under the [PolyForm Shield License 1.0.0](./LICENSE.md), in plain language:\n\n- **Source-available.** The source is public and you can read, fork, and modify it.\n- **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.\n- **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.\n\nSee [LICENSE.md](./LICENSE.md) for the full, binding terms.\n","readmeFilename":"README.md"}