Make VitePress docs fully WCAG 2.1 AA accessible#91
Make VitePress docs fully WCAG 2.1 AA accessible#91shouze merged 8 commits intofeat/promotion-polishfrom
Conversation
- Add ARIA tabs pattern (roving tabindex, keyboard nav) to UseCaseTabs - Add table semantics (caption, scope, aria-label) to ComparisonTable - Fix heading hierarchy and landmarks in InstallSection, ProductionCta, TestimonialsSection, HowItWorks - Fix contrast failures: .ct-feature-desc, .ct-tool-alt, .ts-role → text-1/2; .ts-avatar #CC88FF → #8833cc; .td-ps #9933ff → #aa55ff - Switch Shiki light theme to github-light-high-contrast (fixes #D73A49, #6A737D, #22863A tokens below 4.5:1); add CSS fallback overrides - aria-hidden="true" on decorative TerminalDemo - Add .sr-only utility and global :focus-visible ring to custom.css - Fix hero alt="" (decorative image) in docs/index.md - Add .github/workflows/a11y.yml (pa11y-ci, WCAG2AA, sitemap-driven) - Add .pa11yci.json with WCAG2AA config and F77 ignore (Mermaid SVG ids) - Add docs:a11y and docs:build:a11y scripts (VITEPRESS_HOSTNAME override so sitemap.xml uses localhost URLs during CI audit) - Split mermaid+d3 into dedicated Rollup chunk; raise chunkSizeWarningLimit to 2500 kB to silence legitimate Mermaid size warning - Increase HowItWorks step description font-size 14px → 15px
981dfdf to
82a37b8
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves the accessibility of the VitePress documentation site (WCAG 2.1 AA) and adds automated pa11y-ci auditing in CI to prevent regressions.
Changes:
- Add pa11y-ci configuration + a dedicated GitHub Actions workflow to audit the built docs via the generated sitemap.
- Update multiple VitePress theme components and styles to improve semantics/ARIA, keyboard focus visibility, and contrast.
- Adjust VitePress config for higher-contrast Shiki theme, sitemap hostname override for local/CI, and Rollup chunk splitting for Mermaid.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds docs:build:a11y and docs:a11y scripts to support CI/local accessibility auditing. |
| docs/index.md | Makes the hero image decorative for AT by setting alt: "". |
| docs/.vitepress/theme/custom.css | Adds .sr-only, global :focus-visible ring, and Shiki token contrast fallbacks. |
| docs/.vitepress/theme/UseCaseTabs.vue | Implements WAI-ARIA tabs pattern (roles, roving tabindex, keyboard navigation). |
| docs/.vitepress/theme/TestimonialsSection.vue | Adds section labeling and an explicit link aria-label; improves contrast. |
| docs/.vitepress/theme/TerminalDemo.vue | Marks the terminal animation as decorative (aria-hidden) and tweaks prompt contrast. |
| docs/.vitepress/theme/ProductionCta.vue | Converts wrapper to a labeled landmark section and adds focus-visible styling. |
| docs/.vitepress/theme/InstallSection.vue | Fixes heading hierarchy, hides decorative UI from AT, and adds focus-visible styling. |
| docs/.vitepress/theme/HowItWorks.vue | Adds section labeling and improves typography for readability. |
| docs/.vitepress/theme/ComparisonTable.vue | Adds caption/scope for table semantics and improves contrast of table labels. |
| docs/.vitepress/config.mts | Switches to high-contrast Shiki light theme, adds sitemap hostname override, and splits Mermaid chunk. |
| .pa11yci.json | Introduces pa11y-ci defaults (WCAG2AA) and ignore rules for known non-blocking issues. |
| .gitignore | Ignores the generated a11y-report.json. |
| .github/workflows/docs.yml | Updates action versions/pins for docs workflow steps. |
| .github/workflows/ci.yaml | Updates the pinned setup-bun action reference/comment. |
| .github/workflows/cd.yaml | Updates the pinned setup-bun action reference/comment. |
| .github/workflows/a11y.yml | Adds a new workflow that builds, previews, and audits the docs via pa11y-ci. |
…Table 2-col - custom.css: hide VPNavBarMenu/Extra and show hamburger below 960px; unlock VPNavScreen at 768-960px so the mobile menu actually opens; overflow-x guard on VPHome / pre blocks - ComparisonTable.vue: keep both tool columns at all viewports; abbreviated headers (ct-name-short) on ≤640px; remove 480px column-hiding rule - InstallSection.vue / UseCaseTabs.vue: overflow-x fixes for mobile - ProductionCta.vue: add aria-label on external CTA link; remove duplicate .cta-btn:focus-visible rule - config.mts: remove redundant dynamic import of fileURLToPath (already imported at top)
- scripts/responsive.pw.ts: 4 viewports × 5 pages = 20 tests; overflow detection skips fixed/sticky ancestors and hidden elements; saves screenshot on failure under test-results/screenshots/ - playwright.config.ts: fullyParallel, 4 workers (2 CI), domcontentloaded, 15s timeout → ~24s total for the full suite - package.json: add docs:test:responsive and docs:a11y scripts; VITEPRESS_BASE variable for pa11y-ci; @playwright/test devDependency - knip.json: exclude scripts/responsive.pw.ts from knip analysis - .gitignore: ignore test-results/ and playwright-report/
- responsive.yaml: new workflow running Playwright responsive tests on push/PR; builds VitePress, serves preview, runs bun docs:test:responsive - a11y.yml: fix corrupted setup-bun action pin comment (# v2.1.3) - ci.yaml: same comment fix - cd.yaml: same comment fix - docs.yml: same comment fix (2 occurrences)
…ctions - .github/skills/documentation.md: VitePress theme map, CSS vars, WCAG 2.1 AA patterns, responsive breakpoints, VitePress 768px quirks, validation checklist - .github/skills/feature.md: layer map, type-first design, CLI conventions, render sub-module extension playbook, test patterns - .github/skills/bug-fixing.md: extended symptom→module table, test-first patterns, minimal fix principles - .github/skills/refactoring.md: architectural invariants, safe rename playbook, module extraction pattern, knip interpretation guide - .github/skills/release.md: semver guide, CD pipeline, binary targets, blog post format, CHANGELOG rules, versioned docs snapshot mechanics - All 5 instruction files: add Skill reference header pointing to companion skill - CONTRIBUTING.md: add docs validation commands + AI agent tooling section
76f7b82 to
e4cfea0
Compare
e4cfea0 to
c9226f9
Compare
- UseCaseTabs.vue: fix tabRefs ref callback to write to tabRefs.value[i] so roving-tabindex focus logic works correctly (was writing to tabRefs[i], i.e. the ref wrapper, not the underlying array) - ComparisonTable.vue: add role="img" + aria-hidden on ✓/✗ symbols so AT announces 'Yes'/'No' reliably across AT/browser combos - config.mts: replace dead vitepress-plugin-mermaid pattern with the actual package vitepress-mermaid-renderer in manualChunks rule - responsive.pw.ts: fix header comment (domcontentloaded, not networkidle); derive PATHS from VITEPRESS_BASE env var so tests work on snapshot builds
| # Prevents horizontal overflow regressions at common mobile/tablet viewport sizes. | ||
| # Runs Playwright against a VitePress preview build for every push or PR that | ||
| # touches the docs source or this workflow file. | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - "docs/**" | ||
| - "playwright.config.ts" | ||
| - "scripts/responsive.pw.ts" | ||
| - ".github/workflows/responsive.yaml" |
There was a problem hiding this comment.
This workflow’s header comment says it runs “for every push or PR”, but the on: section only configures pull_request. Either add a push trigger (likely push: { branches: [main], paths: [...] } to match the a11y workflow) or update the comment/PR description so the actual CI coverage is clear.
| "docs:build:a11y": "VITEPRESS_HOSTNAME=http://localhost:4173 vitepress build docs", | ||
| "docs:build:og": "bun scripts/generate-og.ts", | ||
| "docs:preview": "vitepress preview docs" | ||
| "docs:preview": "vitepress preview docs", | ||
| "docs:a11y": "bunx pa11y-ci --config .pa11yci.json --sitemap http://localhost:4173${VITEPRESS_BASE:-/github-code-search/}sitemap.xml", | ||
| "docs:test:responsive": "bunx playwright test" |
There was a problem hiding this comment.
The new docs scripts rely on POSIX-only syntax: VITEPRESS_HOSTNAME=... env var prefixing and ${VITEPRESS_BASE:-...} parameter expansion. This will fail when contributors run bun run docs:* from Windows shells (cmd/PowerShell), and CONTRIBUTING.md now asks docs contributors to run these commands locally. Consider switching to a cross-platform approach (e.g. a small Bun/Node wrapper script that reads env vars and invokes pa11y/vitepress), or use a cross-env-style solution.
| sitemap: { | ||
| hostname: "https://fulll.github.io/github-code-search/", | ||
| hostname: | ||
| (process.env.VITEPRESS_HOSTNAME ?? "https://fulll.github.io") + "/github-code-search/", | ||
| }, |
There was a problem hiding this comment.
sitemap.hostname is hard-coded to "/github-code-search/", but this docs site’s base is derived from process.env.VITEPRESS_BASE. For versioned/snapshot builds (e.g. /github-code-search/v2/) this will generate incorrect sitemap URLs and can break docs:a11y (pa11y follows URLs from the sitemap). Consider composing hostname from VITEPRESS_HOSTNAME + VITEPRESS_BASE (normalized with leading/trailing slash) instead of hard-coding the path segment.
| "$schema": "https://unpkg.com/knip@latest/schema.json", | ||
| "ignore": ["docs/**", "scripts/**"], | ||
| "ignoreDependencies": ["bun-types", "mermaid", "vitepress-mermaid-renderer", "@resvg/resvg-js"], | ||
| "ignore": ["docs/**"], |
There was a problem hiding this comment.
Removing scripts/** from knip's ignore list will cause CI failures if knip flags scripts/generate-og.ts, scripts/responsive.pw.ts, or playwright.config.ts as unused files (they’re executed by tooling, not imported by TS entrypoints). Either re-add scripts/** to ignore or configure knip entry / project so these tooling-driven files are treated as reachable.
| "ignore": ["docs/**"], | |
| "ignore": ["docs/**", "scripts/**"], |
| // Fixed or sticky ancestor: children live in a separate layer and | ||
| // can never contribute to the document horizontal scroll width. | ||
| // VitePress uses position:sticky (not fixed) for .VPNav/.VPNavBar. | ||
| if (s.position === "fixed" || s.position === "sticky") return true; | ||
| parent = parent.parentElement; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| for (const el of document.querySelectorAll("body *")) { | ||
| const s = window.getComputedStyle(el); | ||
| // Skip the element itself if fixed or sticky (same isolation rule) | ||
| if (s.position === "fixed" || s.position === "sticky") continue; | ||
| // Skip invisible elements (closed VitePress flyout panels, etc.) | ||
| if (s.visibility === "hidden" || s.display === "none") continue; |
There was a problem hiding this comment.
The overflow detector treats position: sticky ancestors/elements as “isolated” like fixed and skips them. Unlike fixed, sticky elements still participate in normal layout and can contribute to documentElement.scrollWidth, so this can hide real page-level horizontal overflow regressions (notably VitePress’s sticky nav). Consider removing sticky from the containment/skip rules (or validating overflow via document.documentElement.scrollWidth while filtering out known fixed overlays separately).
Motivation
The homepage and documentation pages had 84 WCAG 2.1 AA violations reported by pa11y-ci. This PR fixes all of them and sets up CI tooling to maintain the accessibility level going forward.
It also ships a Playwright-based responsive regression suite (4 viewports × 5 pages) to ensure no horizontal scroll is introduced on mobile/tablet viewports.
What changed
Semantics / ARIA
tabindex, keyboard navigation (ArrowLeft/ArrowRight/Home/End),aria-controls,aria-labelledby<caption>,scope="col"on<th>elements,aria-label="Yes"/"No"on ✓/✗ icons<p>→<h3>),aria-labelledbyon the section,aria-hiddenon decorative elements<div>→<section aria-labelledby="…">,aria-label="… (opens in a new tab)"on the external CTA linkaria-labelledby, CTA labelaria-labelledbyaria-hidden="true"(purely decorative demo)alt: ""on the hero image (decorative)Contrast (WCAG AA 4.5:1)
.ct-feature-desc,.ct-tool-alt,.ts-role:var(--vp-c-text-3)(2.87:1) →var(--vp-c-text-1/2).ts-avatarDL:#CC88FF(2.46:1) →#8833cc(5.65:1).td-psprompt$:#9933ff(3.91:1) →#aa55ff(5.3:1)github-light→github-light-high-contrast(resolves#D73A49,#6A737D,#22863Aacross all doc pages)Styles
custom.css—.sr-onlyutility class, global:focus-visibleringcustom.css— hide.VPNavBarExtraat ≤960px to fix real horizontal overflow at 768px (tablet portrait) viewportBuild / CI tooling
.github/workflows/a11y.yml— pa11y-ci WCAG2AA audit on all pages, triggered on push/PR touchingdocs/**.pa11yci.json— WCAG2AA config, ignores F77 (Mermaid duplicate SVG IDs, not blocking for AT).github/workflows/responsive.yaml— Playwright responsive tests (4 viewports × 5 pages), triggered on push/PR touchingdocs/**playwright.config.ts—fullyParallel: true, up to 4 workers locally / 2 in CI for fast runsdocs:build:a11y— builds withVITEPRESS_HOSTNAME=http://localhost:4173so the generated sitemap points to localhost (used directly by pa11y-ci)docs:a11y—bunx pa11y-ci --sitemap http://localhost:4173${VITEPRESS_BASE:-/github-code-search/}sitemap.xmldocs:test:responsive—bunx playwright test"mermaid"Rollup chunk +chunkSizeWarningLimit: 2500to silence the legitimate Mermaid size warning (~2.4 MB)How to test
Accessibility (pa11y-ci)
Responsive (Playwright)