You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every scrollable code block on the website should be reachable by keyboard. A <pre> that scrolls horizontally but carries no tabindex can only be scrolled with a pointer, so a keyboard or switch user cannot read the part of the code that is off screen. This is axe's scrollable-region-focusable.
PR #1202 established the rule in website/AGENTS.md and enforced it in website/test/ssr/pre-block-a11y.test.ts, but only on the three marketing pages in that test's PAGES list (/, /what-is-webjs, /why-webjs). Coverage is partial by design and the contract says so. This issue is the rest of the site, which does NOT satisfy the rule today.
Three separate mechanisms produce the gap, and they need different fixes:
The markdown renderer.website/modules/blog/utils/render-post.ts:89 emits <pre class="… overflow-x-auto"> with no tabindex and no accessible name. It is the body renderer for THREE route families: /blog/[slug] (app/blog/[slug]/page.ts:4), /articles/[slug] (app/articles/[slug]/page.ts:3), and /compare/[slug] (app/compare/[slug]/page.ts:3). One edit fixes all three.
The docs and gallery shell.website/lib/ui/docs-shell.ts:129-135 sets .prose-docs pre { … overflow-x: auto }, which makes every <pre> under /docs/** and /ui/** a scroll container. Those <pre> tags are authored inline in the page files and carry no tabindex. docsShell is called from app/docs/layout.ts:157 and app/ui/layout.ts:65. This is the large one: a scan during the feat(website): sell what a WebJs prompt no longer has to specify #1202 review counted roughly 473 bare <pre> tags under app/docs/.
The error page.website/app/error.ts:26 renders <pre class="w-full mb-8 text-sm overflow-x-auto"> with no tabindex and no name.
Design / approach
Two of the three are single-point fixes. The docs and gallery set is the real work, and a per-tag mechanical sweep of ~473 tags is both tedious and prone to drifting back, so prefer extracting a shared emitter.
Suggested shape, matching how this site already factors composed fragments (website/AGENTS.md "How lib/ grows" says one fragment per file under lib/ui/):
Add website/lib/ui/code-block.ts exporting a function that returns the correct <pre> markup: tabindex="0" always, plus role="region" and an aria-label when the caller supplies a name. Route the markdown renderer and the docs and gallery pages through it.
Do NOT add an aria-label without role="region". A <pre> maps to ARIA role generic, where ARIA prohibits an author-supplied name, so a name without the role is a name no spec-following screen reader announces. That is rule 1 of the same three, and adding names carelessly here would trade one violation for another. A block with no good name should take tabindex="0" alone and no name, which is valid.
Names must be unique per page (rule 2). A named region is a landmark, and two landmarks sharing a name collapse into an ambiguous pair. This already bit feat(website): sell what a WebJs prompt no longer has to specify #1202: two cards on the home page both said aria-label="Example files" and had to be named apart. If a generic name is the only option for a docs page with many blocks, prefer no name over a repeated one.
Do the work in stages so each is reviewable and each can extend the test's PAGES list as it lands: the markdown renderer plus the error page first (small, three route families plus one page), then the docs and gallery sweep.
Implementation notes (for the implementing agent)
Where to edit
website/modules/blog/utils/render-post.ts around L89, the out.push('<pre class="…">') call in the fenced-code branch. Fixes /blog/[slug], /articles/[slug], /compare/[slug] at once.
website/app/error.ts L26.
website/app/docs/**/page.ts and website/app/ui/**/page.ts, every inline <pre>. website/lib/ui/docs-shell.ts:129-135 is the CSS that makes them scroll; the CSS itself does not need to change, since tabindex cannot be set from a stylesheet.
website/test/ssr/pre-block-a11y.test.ts, the PAGES array near the top. Add each page or route family as it is brought into line. The file's three tests (role for a named block, unique names, focus stop on a scrollable block) already express the rules and need no change to cover a new page.
website/AGENTS.md, the Style-section bullet added by feat(website): sell what a WebJs prompt no longer has to specify #1202. It currently says coverage is PARTIAL and explicitly disclaims being an inventory. Update or remove that disclaimer only when coverage is genuinely complete.
Landmines / gotchas
Do not reintroduce the two mistakes feat(website): sell what a WebJs prompt no longer has to specify #1202 already made here. A name without role="region" is prohibited by ARIA on generic; two blocks sharing a name produce duplicate landmarks. Both were review findings on that PR.
The scrollability detector in the test reads the overflow-x-auto UTILITY CLASS, so it does NOT see a block scrolled only by the .prose-docs pre stylesheet rule. Adding a docs page to PAGES will therefore PASS rule 3 vacuously while the page is still broken. Either give those blocks the utility class as part of the fix, or extend the detector to treat a page rendered through docsShell as scrollable. This is the single most likely way to close the issue without fixing anything.
renderPostBody is shared by three route families, so verify all three, not just /blog.
Adding tabindex="0" inserts a tab stop. On a docs page with many blocks that is a lot of stops, which is the accepted tradeoff for the WCAG requirement, but sanity-check the tab order on one long page rather than assuming.
Invariants to respect
website/AGENTS.md Style section: light DOM, Tailwind utilities, @theme tokens, one <section> per page section, plus the three code-block rules themselves.
Root AGENTS.md invariant 9 (no backtick characters inside an html template body) and invariant 11 (prose punctuation and WebJs brand casing) apply to any comment or doc prose added.
Tests + doc surfaces
website/test/ssr/pre-block-a11y.test.ts is the layer. Extend PAGES, and prove the counterfactual per page family: reverting the fix must red the rule-3 assertion for that page, NOT pass vacuously (see the landmine above).
No packages/*/src change, so the framework doc surfaces, the scaffold, the MCP, and the changelog are all N/A. This is website-only.
Acceptance criteria
Every <pre> that scrolls on /blog/[slug], /articles/[slug], /compare/[slug], and the error page carries tabindex="0"
Every <pre> that scrolls under /docs/** and /ui/** carries tabindex="0"
No block gains an aria-label without role="region", and no two blocks on one page share a name
PAGES in pre-block-a11y.test.ts covers every page family brought into line
A counterfactual proves the rule-3 assertion actually fires for a docs page, rather than passing vacuously because the detector cannot see a stylesheet-driven scroll container
The partial-coverage disclaimer in website/AGENTS.md is updated to match the real state, and removed only if coverage becomes complete
The website test suite passes and webjs check is clean
Problem
Every scrollable code block on the website should be reachable by keyboard. A
<pre>that scrolls horizontally but carries notabindexcan only be scrolled with a pointer, so a keyboard or switch user cannot read the part of the code that is off screen. This is axe'sscrollable-region-focusable.PR #1202 established the rule in
website/AGENTS.mdand enforced it inwebsite/test/ssr/pre-block-a11y.test.ts, but only on the three marketing pages in that test'sPAGESlist (/,/what-is-webjs,/why-webjs). Coverage is partial by design and the contract says so. This issue is the rest of the site, which does NOT satisfy the rule today.Three separate mechanisms produce the gap, and they need different fixes:
website/modules/blog/utils/render-post.ts:89emits<pre class="… overflow-x-auto">with notabindexand no accessible name. It is the body renderer for THREE route families:/blog/[slug](app/blog/[slug]/page.ts:4),/articles/[slug](app/articles/[slug]/page.ts:3), and/compare/[slug](app/compare/[slug]/page.ts:3). One edit fixes all three.website/lib/ui/docs-shell.ts:129-135sets.prose-docs pre { … overflow-x: auto }, which makes every<pre>under/docs/**and/ui/**a scroll container. Those<pre>tags are authored inline in the page files and carry notabindex.docsShellis called fromapp/docs/layout.ts:157andapp/ui/layout.ts:65. This is the large one: a scan during the feat(website): sell what a WebJs prompt no longer has to specify #1202 review counted roughly 473 bare<pre>tags underapp/docs/.website/app/error.ts:26renders<pre class="w-full mb-8 text-sm overflow-x-auto">with notabindexand no name.Design / approach
Two of the three are single-point fixes. The docs and gallery set is the real work, and a per-tag mechanical sweep of ~473 tags is both tedious and prone to drifting back, so prefer extracting a shared emitter.
Suggested shape, matching how this site already factors composed fragments (
website/AGENTS.md"How lib/ grows" says one fragment per file underlib/ui/):website/lib/ui/code-block.tsexporting a function that returns the correct<pre>markup:tabindex="0"always, plusrole="region"and anaria-labelwhen the caller supplies a name. Route the markdown renderer and the docs and gallery pages through it.aria-labelwithoutrole="region". A<pre>maps to ARIA rolegeneric, where ARIA prohibits an author-supplied name, so a name without the role is a name no spec-following screen reader announces. That is rule 1 of the same three, and adding names carelessly here would trade one violation for another. A block with no good name should taketabindex="0"alone and no name, which is valid.aria-label="Example files"and had to be named apart. If a generic name is the only option for a docs page with many blocks, prefer no name over a repeated one.Do the work in stages so each is reviewable and each can extend the test's
PAGESlist as it lands: the markdown renderer plus the error page first (small, three route families plus one page), then the docs and gallery sweep.Implementation notes (for the implementing agent)
Where to edit
website/modules/blog/utils/render-post.tsaround L89, theout.push('<pre class="…">')call in the fenced-code branch. Fixes/blog/[slug],/articles/[slug],/compare/[slug]at once.website/app/error.tsL26.website/app/docs/**/page.tsandwebsite/app/ui/**/page.ts, every inline<pre>.website/lib/ui/docs-shell.ts:129-135is the CSS that makes them scroll; the CSS itself does not need to change, sincetabindexcannot be set from a stylesheet.website/test/ssr/pre-block-a11y.test.ts, thePAGESarray near the top. Add each page or route family as it is brought into line. The file's three tests (role for a named block, unique names, focus stop on a scrollable block) already express the rules and need no change to cover a new page.website/AGENTS.md, the Style-section bullet added by feat(website): sell what a WebJs prompt no longer has to specify #1202. It currently says coverage is PARTIAL and explicitly disclaims being an inventory. Update or remove that disclaimer only when coverage is genuinely complete.Landmines / gotchas
role="region"is prohibited by ARIA ongeneric; two blocks sharing a name produce duplicate landmarks. Both were review findings on that PR.overflow-x-autoUTILITY CLASS, so it does NOT see a block scrolled only by the.prose-docs prestylesheet rule. Adding a docs page toPAGESwill therefore PASS rule 3 vacuously while the page is still broken. Either give those blocks the utility class as part of the fix, or extend the detector to treat a page rendered throughdocsShellas scrollable. This is the single most likely way to close the issue without fixing anything.renderPostBodyis shared by three route families, so verify all three, not just/blog.tabindex="0"inserts a tab stop. On a docs page with many blocks that is a lot of stops, which is the accepted tradeoff for the WCAG requirement, but sanity-check the tab order on one long page rather than assuming.Invariants to respect
website/AGENTS.mdStyle section: light DOM, Tailwind utilities,@themetokens, one<section>per page section, plus the three code-block rules themselves.AGENTS.mdinvariant 9 (no backtick characters inside anhtmltemplate body) and invariant 11 (prose punctuation andWebJsbrand casing) apply to any comment or doc prose added.Tests + doc surfaces
website/test/ssr/pre-block-a11y.test.tsis the layer. ExtendPAGES, and prove the counterfactual per page family: reverting the fix must red the rule-3 assertion for that page, NOT pass vacuously (see the landmine above).packages/*/srcchange, so the framework doc surfaces, the scaffold, the MCP, and the changelog are all N/A. This is website-only.Acceptance criteria
<pre>that scrolls on/blog/[slug],/articles/[slug],/compare/[slug], and the error page carriestabindex="0"<pre>that scrolls under/docs/**and/ui/**carriestabindex="0"aria-labelwithoutrole="region", and no two blocks on one page share a namePAGESinpre-block-a11y.test.tscovers every page family brought into linewebsite/AGENTS.mdis updated to match the real state, and removed only if coverage becomes completewebjs checkis clean