Skip to content

Make VitePress docs fully WCAG 2.1 AA accessible#91

Merged
shouze merged 8 commits intofeat/promotion-polishfrom
feat/accessibility-homepage
Mar 8, 2026
Merged

Make VitePress docs fully WCAG 2.1 AA accessible#91
shouze merged 8 commits intofeat/promotion-polishfrom
feat/accessibility-homepage

Conversation

@shouze
Copy link
Contributor

@shouze shouze commented Mar 8, 2026

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

  • UseCaseTabs — full WAI-ARIA Tabs pattern: roving tabindex, keyboard navigation (ArrowLeft / ArrowRight / Home / End), aria-controls, aria-labelledby
  • ComparisonTable<caption>, scope="col" on <th> elements, aria-label="Yes" / "No" on ✓/✗ icons
  • InstallSection — corrected heading hierarchy (<p><h3>), aria-labelledby on the section, aria-hidden on decorative elements
  • ProductionCta<div><section aria-labelledby="…">, aria-label="… (opens in a new tab)" on the external CTA link
  • TestimonialsSection — landmark aria-labelledby, CTA label
  • HowItWorks — landmark aria-labelledby
  • TerminalDemoaria-hidden="true" (purely decorative demo)
  • docs/index.mdalt: "" 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-avatar DL: #CC88FF (2.46:1) → #8833cc (5.65:1)
  • .td-ps prompt $: #9933ff (3.91:1) → #aa55ff (5.3:1)
  • Shiki theme: github-lightgithub-light-high-contrast (resolves #D73A49, #6A737D, #22863A across all doc pages)
  • CSS fallback overrides for residual Shiki tokens

Styles

  • custom.css.sr-only utility class, global :focus-visible ring
  • custom.css — hide .VPNavBarExtra at ≤960px to fix real horizontal overflow at 768px (tablet portrait) viewport
  • HowItWorks — description font sizes: 14px → 15px (line-height 1.7 → 1.75), mobile title 15px → 16px
  • ComparisonTable — mobile tweaks: hide feature descriptions at ≤640px, hide the "gh search code" column at ≤480px
  • UseCaseTabs — terminal block scrolls internally on mobile instead of pushing the page
  • InstallSection — install terminal blocks constrained to viewport width on mobile

Build / CI tooling

  • .github/workflows/a11y.yml — pa11y-ci WCAG2AA audit on all pages, triggered on push/PR touching docs/**
  • .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 touching docs/**
  • playwright.config.tsfullyParallel: true, up to 4 workers locally / 2 in CI for fast runs
  • docs:build:a11y — builds with VITEPRESS_HOSTNAME=http://localhost:4173 so the generated sitemap points to localhost (used directly by pa11y-ci)
  • docs:a11ybunx pa11y-ci --sitemap http://localhost:4173${VITEPRESS_BASE:-/github-code-search/}sitemap.xml
  • docs:test:responsivebunx playwright test
  • Dedicated "mermaid" Rollup chunk + chunkSizeWarningLimit: 2500 to silence the legitimate Mermaid size warning (~2.4 MB)

How to test

Accessibility (pa11y-ci)

bun run docs:build:a11y
bun run docs:preview -- --port 4173 &
bun run docs:a11y   # should report 0 errors

Responsive (Playwright)

bun run docs:build
bun run docs:preview -- --port 4173 &
bun run docs:test:responsive   # 20 tests (4 viewports × 5 pages), should all pass

Copilot AI review requested due to automatic review settings March 8, 2026 16:08
@shouze shouze self-assigned this Mar 8, 2026
- 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
@shouze shouze force-pushed the feat/accessibility-homepage branch from 981dfdf to 82a37b8 Compare March 8, 2026 16:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

shouze added 4 commits March 8, 2026 18:34
…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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated 6 comments.

@shouze shouze force-pushed the feat/accessibility-homepage branch 2 times, most recently from 76f7b82 to e4cfea0 Compare March 8, 2026 17:46
@shouze shouze force-pushed the feat/accessibility-homepage branch from e4cfea0 to c9226f9 Compare March 8, 2026 17:47
@shouze shouze requested a review from Copilot March 8, 2026 17:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated 10 comments.

shouze added 2 commits March 8, 2026 18:57
- 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
@shouze shouze requested a review from Copilot March 8, 2026 18:10
@shouze shouze merged commit 6be130d into feat/promotion-polish Mar 8, 2026
4 checks passed
@shouze shouze deleted the feat/accessibility-homepage branch March 8, 2026 18:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 33 changed files in this pull request and generated 5 comments.

Comment on lines +3 to +13
# 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"
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
"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"
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 353 to 356
sitemap: {
hostname: "https://fulll.github.io/github-code-search/",
hostname:
(process.env.VITEPRESS_HOSTNAME ?? "https://fulll.github.io") + "/github-code-search/",
},
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
"$schema": "https://unpkg.com/knip@latest/schema.json",
"ignore": ["docs/**", "scripts/**"],
"ignoreDependencies": ["bun-types", "mermaid", "vitepress-mermaid-renderer", "@resvg/resvg-js"],
"ignore": ["docs/**"],
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"ignore": ["docs/**"],
"ignore": ["docs/**", "scripts/**"],

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +109
// 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;
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@shouze shouze restored the feat/accessibility-homepage branch March 8, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants