Skip to content

Improve apex accessibility and add a route-wide a11y gate#573

Open
isuttell wants to merge 1 commit into
mainfrom
codex/ap-387-apex-accessibility-gate
Open

Improve apex accessibility and add a route-wide a11y gate#573
isuttell wants to merge 1 commit into
mainfrom
codex/ap-387-apex-accessibility-gate

Conversation

@isuttell

@isuttell isuttell commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix the easy apex accessibility issues found during review and add a repeatable Lighthouse gate so the marketing surface stays at 100 across prerendered routes.

Changes

  • Improve apex CTA/button contrast with shared primary tokens and higher-contrast footer/demo text.
  • Add a skip link and stable main-content targets across apex pages.
  • Fix the transcript demo result link accessible name to include the visible URL.
  • Add pnpm lighthouse:apex-a11y and document the route-wide accessibility check.

Risk: LOW

  • Areas touched: apex UI, shared brand/UI button tokens, docs, local scripts.
  • Security: no auth, secrets, data, or permission paths changed.
  • Performance: local Lighthouse script only; runtime CSS/token changes are minimal.
  • Breaking: none expected.

Test plan

  • pnpm format:docs:check
  • pnpm verify
  • pnpm lighthouse:apex-a11y
  • pnpm test:coverage
  • pnpm smoke:local
  • pre-push pnpm test:coverage:strict
  • pre-push pnpm verify
  • Chrome smoke on built apex home page: skip link target, CTA colors, and result link label

Issue: AP-387

Summary by CodeRabbit

  • New Features

    • Added skip to main content link for keyboard navigation
    • Added automated Lighthouse accessibility testing
  • Bug Fixes

    • Improved focus targeting across main content regions
    • Enhanced accessibility labels for better screen reader clarity
    • Refined footer text color for improved readability
  • Tests

    • Added accessibility compliance checks for all routes
  • Chores

    • Updated development documentation with new testing guidance

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds accessibility skip-to-content infrastructure (skip link in Shell, id="main-content" + tabIndex={-1} on all Apex page <main> elements), introduces --primary CSS color tokens in the UI and brand packages, updates button variant/size classes, makes minor a11y tweaks, and adds a Lighthouse accessibility gate script with docs and wiring.

Changes

Apex Accessibility, Design Tokens, and Lighthouse Gate

Layer / File(s) Summary
Primary color token definitions
packages/ui/src/styles/globals.css, packages/brand/src/index.ts
Adds --primary/--primary-fg CSS custom properties aliased to --foreground/--background in dark and light theme blocks, and exposes --color-primary/--color-primary-foreground in the Tailwind @theme layer.
Button variant and size class updates
packages/ui/src/components/buttonClasses.ts
Updates VARIANTS to use bg-primary/!text-primary-foreground for the primary variant with hover:bg-muted, adds !-prefixed text color overrides in other variants, and replaces text-mono/text-base in SIZES with explicit CSS-variable-based font-size/line-height classes.
Skip link and main-content landmarks
apps/apex/src/app/Shell.tsx, apps/apex/src/pages/... (7 pages)
Adds a visually-hidden focusable "Skip to content" link targeting #main-content in Shell. Adds id="main-content" and tabIndex={-1} to the root <main> in HomePage, AboutPage, DocsIndexPage, DocsPageView, HowItWorksPage, LegalPage, and PricingPage.
Minor a11y and styling tweaks
apps/apex/src/pages/HomePage.tsx, apps/apex/src/components/TranscriptDemo.tsx, apps/apex/src/app/chrome.tsx
Removes aria-hidden="true" from the CommandBox label, updates HERO_CTA to the primary button style, shortens the TranscriptDemo result link aria-label to "Open example: https://…", and changes footer text colors from text-faint/text-faint/70 to text-subtle.
Lighthouse accessibility gate script
scripts/lighthouse-apex-a11y.mjs
New Node.js script that serves the built Apex client locally, dynamically imports the built route table, runs Lighthouse in the accessibility category for each route via headless Chrome, and exits non-zero if any route scores below the configurable threshold.
Tests, package script, and docs
apps/apex/src/render.test.tsx, package.json, docs/development.md, scripts/README.md
Adds render tests for the skip link and updated aria-label format. Registers lighthouse:apex-a11y in package.json, documents it in the Smoke Tests table, and adds a scripts/README.md subsection describing the script's behavior and environment variable override.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • zaks-io/agent-paste#428: Modifies the same footer install-link/label region in apps/apex/src/app/chrome.tsx that this PR updates with text-subtle styling.
  • zaks-io/agent-paste#458: Adds the TranscriptDemo result link including its original aria-label, which this PR revises to the shorter "Open example: …" phrasing.
  • zaks-io/agent-paste#572: Substantially rewrites TranscriptDemo's staged demo markup and result line/link content, directly overlapping with this PR's aria-label update to that same link.

Poem

🐰 Hop, hop, hooray — the skip link is here,
Each main-content landmark standing crystal clear.
Primary tokens now shine through the theme,
Lighthouse checks each route's a11y dream.
No barrier too high, no focus astray —
The bunny has paved an accessible way! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: accessibility improvements and a new Lighthouse accessibility gate for apex.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/ap-387-apex-accessibility-gate

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.0)
packages/ui/src/styles/globals.css

File contains syntax errors that prevent linting: Line 165: Tailwind-specific syntax is disabled.


Comment @coderabbitai help to get the list of available commands and usage tips.

createReadStream(file).pipe(response);
} catch (error) {
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
response.end(error instanceof Error ? error.message : String(error));

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/apex/src/app/Shell.tsx`:
- Around line 88-93: The className attribute on the skip-to-content anchor
element contains focus:not-sr-only, which is not a valid Tailwind v4 utility
class and will be silently ignored with no effect. Remove the focus:not-sr-only
class from the className string, keeping the other focus variants (focus:fixed,
focus:top-4, focus:left-4, etc.) which properly override the sr-only positioning
when the link receives focus.

In `@apps/apex/src/pages/HomePage.tsx`:
- Line 27: The important modifier syntax in the string on line 27 uses Tailwind
v3 format where the exclamation mark prefix appears before the utility name
(e.g., !text-primary-foreground). In Tailwind v4, the important modifier must be
placed after the utility name instead. Change !text-primary-foreground to
text-primary-foreground! so the important modifier correctly applies the high
specificity to the primary foreground text color in the className string that
includes bg-primary and border utilities.

In `@scripts/lighthouse-apex-a11y.mjs`:
- Line 12: The failure condition around line 136 (which checks if audits.length
> 0) always fails regardless of the minScore threshold set on line 12,
effectively bypassing the AGENT_PASTE_LIGHTHOUSE_APEX_A11Y_MIN_SCORE environment
variable override. Modify the failure condition to respect the minScore
threshold so that lowering the threshold value actually allows the check to pass
when audit scores meet the specified minimum score rather than always failing
when audits exist.
- Around line 80-83: The file stream created by
createReadStream(file).pipe(response) is not handling stream-level errors, which
can cause unhandled errors if the file becomes unreadable after path resolution.
Store the stream returned by createReadStream in a variable and attach an error
event listener to it to handle any stream errors that occur (such as file access
issues). The error handler should respond with an appropriate error status code
and message to the client, similar to the existing try-catch error handling, to
prevent the process from terminating unexpectedly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ea9d6429-d683-4bef-b330-c341df449219

📥 Commits

Reviewing files that changed from the base of the PR and between ca90140 and 8ad4d35.

📒 Files selected for processing (18)
  • apps/apex/src/app/Shell.tsx
  • apps/apex/src/app/chrome.tsx
  • apps/apex/src/components/TranscriptDemo.tsx
  • apps/apex/src/pages/AboutPage.tsx
  • apps/apex/src/pages/DocsIndexPage.tsx
  • apps/apex/src/pages/DocsPageView.tsx
  • apps/apex/src/pages/HomePage.tsx
  • apps/apex/src/pages/HowItWorksPage.tsx
  • apps/apex/src/pages/LegalPage.tsx
  • apps/apex/src/pages/PricingPage.tsx
  • apps/apex/src/render.test.tsx
  • docs/development.md
  • package.json
  • packages/brand/src/index.ts
  • packages/ui/src/components/buttonClasses.ts
  • packages/ui/src/styles/globals.css
  • scripts/README.md
  • scripts/lighthouse-apex-a11y.mjs

Comment on lines +88 to +93
<a
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-[60] focus:rounded-xs focus:border focus:border-rule-strong focus:bg-surface focus:px-3 focus:py-2 focus:font-mono focus:text-mono-sm focus:text-foreground"
href="#main-content"
>
Skip to content
</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for not-sr-only utility definition in CSS files
rg -n 'not-sr-only' --type=css --type=scss -g '!node_modules/**'

# Also check globals.css and any Tailwind config
fd -e css -e ts -e js globals tailwind.config | xargs rg -n 'not-sr-only'

Repository: zaks-io/agent-paste

Length of output: 493


🏁 Script executed:

# Find tailwind config file
fd -type f -name "tailwind.config*" 

# Check package.json for Tailwind version
cat package.json | grep -i "tailwind\|postcss"

# Search for sr-only related definitions
rg -n "sr-only" --type=ts --type=js --type=css -A 2

# Look for custom utility definitions
rg -n "extend.*utilities\|addUtilities" --type=ts --type=js

Repository: zaks-io/agent-paste

Length of output: 1155


🏁 Script executed:

# Find tailwind config file
find . -maxdepth 3 -name "tailwind.config.*" -type f 2>/dev/null

# Check package.json for Tailwind version and dependencies
rg "tailwind|postcss" package.json -A 1 -B 1

# Search more broadly for custom utilities or not-sr-only definition
rg "not-sr-only|addUtilities|sr-only.*:" --type=ts --type=js -A 2 -B 2

# Check for any CSS definitions of sr-only or not-sr-only
rg "\.sr-only|`@apply`.*sr-only" --type=css

Repository: zaks-io/agent-paste

Length of output: 616


🏁 Script executed:

# List all config files in the repo
ls -la | grep -E "tailwind|postcss|package"

# Find tailwind.config in any location
find . -type f -name "tailwind.config*" 2>/dev/null | head -20

# Check for package.json files
find . -type f -name "package.json" -path "*/apps/*" | head -5

# Read the main or apex package.json
cat apps/apex/package.json 2>/dev/null | grep -A 5 -B 5 "tailwind\|postcss" || echo "Not in apex package.json, checking root"

# Check root package.json
cat package.json 2>/dev/null | grep -A 5 -B 5 "tailwind\|postcss"

# Look for any CSS that might define sr-only or utilities
find . -type f \( -name "*.css" -o -name "*.scss" \) ! -path "*/node_modules/*" | head -20

Repository: zaks-io/agent-paste

Length of output: 895


🏁 Script executed:

# Find tailwind.config file - should be in root or apps directory
find . -maxdepth 3 -type f -name "tailwind.config*" 2>/dev/null

# Check the apex CSS files for any custom utilities
cat apps/apex/src/styles/apex.css

# Check the ui package globals.css
cat packages/ui/src/styles/globals.css

# Check root globals.css if it exists
cat apps/web/src/styles/globals.css | head -50

# Look for any tailwind config in the root
ls -la tailwind* 2>/dev/null || echo "No tailwind config in root"

# Check if there's a postcss config
cat postcss.config.* 2>/dev/null || echo "No postcss config found"

Repository: zaks-io/agent-paste

Length of output: 19960


🏁 Script executed:

# Final check: search for any Tailwind plugin or extend configuration
rg "addUtilities|extend.*utilities|plugin" packages/ui/ --type=ts --type=js -A 3 -B 1

# Check if there's a vite config that might extend Tailwind
cat apps/apex/vite.config.* 2>/dev/null || echo "No explicit vite config in apex"

# Check packages/ui for any Tailwind config
find packages/ui -name "vite.config.*" -o -name "tailwind.config.*" 2>/dev/null

Repository: zaks-io/agent-paste

Length of output: 628


Remove focus:not-sr-only — it's not a valid Tailwind utility.

The class focus:not-sr-only is not defined anywhere in the codebase and is not part of Tailwind v4's standard utilities. In Tailwind v4, the not-* variant is for negating state selectors (e.g., not-hover:), not for negating utility classes. Since it doesn't exist, this class will be silently ignored and have no effect.

The subsequent focus variants (focus:fixed, focus:top-4, etc.) will override most sr-only positioning, but they don't fully reverse all constraints like clip, overflow, and dimensions. Remove the unused class or define it as a custom utility if not-sr-only is intentionally needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/apex/src/app/Shell.tsx` around lines 88 - 93, The className attribute on
the skip-to-content anchor element contains focus:not-sr-only, which is not a
valid Tailwind v4 utility class and will be silently ignored with no effect.
Remove the focus:not-sr-only class from the className string, keeping the other
focus variants (focus:fixed, focus:top-4, focus:left-4, etc.) which properly
override the sr-only positioning when the link receives focus.

const HERO_CTA =
"group inline-flex items-center gap-2 font-ui font-semibold text-base " +
"text-accent-foreground bg-accent border border-accent " +
"!text-primary-foreground bg-primary border border-primary " +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix the Tailwind v4 important modifier syntax.

The important modifier !text-primary-foreground uses Tailwind v3 syntax with the ! prefix. In Tailwind v4, the important modifier must appear after the utility name. This line will not apply the important text color as intended.

🔧 Proposed fix
-  "!text-primary-foreground bg-primary border border-primary " +
+  "text-primary-foreground! bg-primary border border-primary " +
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"!text-primary-foreground bg-primary border border-primary " +
"text-primary-foreground! bg-primary border border-primary " +
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/apex/src/pages/HomePage.tsx` at line 27, The important modifier syntax
in the string on line 27 uses Tailwind v3 format where the exclamation mark
prefix appears before the utility name (e.g., !text-primary-foreground). In
Tailwind v4, the important modifier must be placed after the utility name
instead. Change !text-primary-foreground to text-primary-foreground! so the
important modifier correctly applies the high specificity to the primary
foreground text color in the className string that includes bg-primary and
border utilities.

const root = resolve(import.meta.dirname, "..");
const clientDir = resolve(root, "apps/apex/dist/client");
const serverEntry = resolve(root, "apps/apex/dist/server/entry-server.js");
const minScore = Number(process.env.AGENT_PASTE_LIGHTHOUSE_APEX_A11Y_MIN_SCORE ?? "100");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Threshold override is effectively bypassed by the failure condition.

Line 136 always fails when audits.length > 0, so lowering AGENT_PASTE_LIGHTHOUSE_APEX_A11Y_MIN_SCORE on Line 12 does not actually relax pass criteria as documented.

Proposed fix
 const minScore = Number(process.env.AGENT_PASTE_LIGHTHOUSE_APEX_A11Y_MIN_SCORE ?? "100");
+if (!Number.isFinite(minScore) || minScore < 0 || minScore > 100) {
+  throw new Error("AGENT_PASTE_LIGHTHOUSE_APEX_A11Y_MIN_SCORE must be a number between 0 and 100");
+}
@@
-    const score = Math.round((lhr?.categories?.accessibility?.score ?? 0) * 100);
+    const score = (lhr?.categories?.accessibility?.score ?? 0) * 100;
+    const roundedScore = Math.round(score);
     const audits = lhr ? failedAudits(lhr) : ["missing-lighthouse-result"];
-    process.stdout.write(`${path}: ${score}${audits.length ? ` (${audits.join(", ")})` : ""}\n`);
-    if (score < minScore || audits.length > 0) {
-      failures.push({ path, score, audits });
+    process.stdout.write(`${path}: ${roundedScore}${audits.length ? ` (${audits.join(", ")})` : ""}\n`);
+    if (score < minScore) {
+      failures.push({ path, score: roundedScore, audits });
     }

Also applies to: 133-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lighthouse-apex-a11y.mjs` at line 12, The failure condition around
line 136 (which checks if audits.length > 0) always fails regardless of the
minScore threshold set on line 12, effectively bypassing the
AGENT_PASTE_LIGHTHOUSE_APEX_A11Y_MIN_SCORE environment variable override. Modify
the failure condition to respect the minScore threshold so that lowering the
threshold value actually allows the check to pass when audit scores meet the
specified minimum score rather than always failing when audits exist.

Comment on lines +80 to +83
createReadStream(file).pipe(response);
} catch (error) {
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
response.end(error instanceof Error ? error.message : String(error));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle file stream errors to prevent unexpected process termination.

Line 80 pipes a file stream without handling stream-level errors. If the file becomes unreadable after path resolution, this can emit an unhandled error and fail the gate run abruptly.

Proposed fix
 import { createReadStream } from "node:fs";
 import { access, stat } from "node:fs/promises";
 import { createServer } from "node:http";
 import { extname, join, normalize, resolve, sep } from "node:path";
+import { pipeline } from "node:stream/promises";
@@
-      response.writeHead(200, { "content-type": TYPES.get(extname(file)) ?? "application/octet-stream" });
-      createReadStream(file).pipe(response);
+      response.writeHead(200, { "content-type": TYPES.get(extname(file)) ?? "application/octet-stream" });
+      await pipeline(createReadStream(file), response);
     } catch (error) {
-      response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
-      response.end(error instanceof Error ? error.message : String(error));
+      if (!response.headersSent) {
+        response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
+        response.end("Internal Server Error");
+      } else {
+        response.destroy(error instanceof Error ? error : new Error(String(error)));
+      }
     }
🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 83-83: Information exposure through a stack trace
This information exposed to the user depends on stack trace information.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lighthouse-apex-a11y.mjs` around lines 80 - 83, The file stream
created by createReadStream(file).pipe(response) is not handling stream-level
errors, which can cause unhandled errors if the file becomes unreadable after
path resolution. Store the stream returned by createReadStream in a variable and
attach an error event listener to it to handle any stream errors that occur
(such as file access issues). The error handler should respond with an
appropriate error status code and message to the client, similar to the existing
try-catch error handling, to prevent the process from terminating unexpectedly.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

First-pass review (AP-387)

Risk: medium
Decision: approve

Ticket triage

  • Intended change: Fix apex Lighthouse accessibility issues (contrast, skip link, landmark targets, transcript link name, command-box labels) and add a local route-wide pnpm lighthouse:apex-a11y gate.
  • Scope match: Yes — all listed scope items and acceptance criteria are addressed.

Review summary

Focused a11y improvements with a sensible local gate. I verified pnpm lighthouse:apex-a11y passes 100 on all 15 prerendered routes after build, and focused @agent-paste/apex, @agent-paste/ui, and @agent-paste/brand tests pass. CI Validate and Postgres smoke are green on this PR.

Highlights

  • Skip link + stable #main-content landmarks on every apex page
  • Transcript demo aria-label now includes the visible URL (https://…)
  • Command-box labels exposed to assistive tech (no longer aria-hidden)
  • New scripts/lighthouse-apex-a11y.mjs mirrors the dashboard gate pattern with path traversal guards

Non-blocking follow-ups

  • buttonClasses / new --primary tokens change default primary buttons on the web dashboard too (vermilion → high-contrast neutral). Intentional for contrast, but worth a quick visual check on web CTAs before release.
  • Apex a11y gate is local-only today (dashboard gate runs in CI). Fine for this ticket; consider CI wiring later.

No blocking correctness, security, or regression issues found for first-pass approval.

Open in Web View Automation 

Sent by Cursor Automation: First Pass PR Reviewer

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