Skip to content

A11Y-001: a link is not wrapped around a button - #1002

Merged
NewWorldVenture merged 1 commit into
mainfrom
claude/a11y-001-link-not-around-button-etzqg7
Oct 11, 2026
Merged

NewWorldVenture merged 1 commit into
mainfrom
claude/a11y-001-link-not-around-button-etzqg7

Conversation

@NewWorldVenture

@NewWorldVenture NewWorldVenture commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

A11Y-001: from an axe sweep of every static public route, in both themes at 1280 and 390 px, run against current main (209e5a4).

The defect

Nine call sites rendered a Button as the child of a Link:

  • the 404 page ("Go to dashboard", "Back to home");
  • the error page;
  • the in-app not-found page;
  • the marketing AI page and the shared marketing CTA (on every page that ends with it);
  • two vacations views;
  • the invite screen.

A button inside a link is one action with two controls:

  • Keyboard: two tab stops per action. Measured on main's 404 page in Chromium, Tab goes: A "Go to dashboard", BUTTON "Go to dashboard", A "Back to home", BUTTON "Back to home".
  • HTML: interactive content inside <a> is invalid, and assistive technology announces a button inside a link.
  • Target: the button covers its own link. axe reported the 404 page's "Back to home" as a 9.2 × 44 px target at 390 px (target-size, the one serious finding the public sweep found in either theme; /resources/benchmarks returns that 404 until it is published).

The fix

  • components/ui/button-link.tsx: ButtonLink is a Next Link carrying Button's own classes (variant, size, focus ring, the 44 px coarse-pointer minimum). It is one control.
  • Button's class strings are copied rather than imported, because components/ui/button.tsx exports none and I've left it as is. A test holds the copies equal to Button's.
  • The 404, error, in-app not-found, marketing AI, CTA and both vacations sites use it.

Left on purpose: components/auth/join-invite.tsx (three sites on the invite screen). It's an auth surface, so I've left it for you. The guard test names it, so it can't be forgotten or multiply, and fixing it makes the test ask for the entry to be removed.

Tests

tests/a-link-is-not-wrapped-around-a-button.test.ts

  • No link in app/ or components/ has a button as its child, across line breaks, except the one named file. On main it lists nine files.
  • ButtonLink renders a single <a> with no <button>, and with Button's focus ring.
  • ButtonLink's variants, sizes and base classes equal Button's, read from both sources.
Check main This branch
Guard test fails, 9 files 3/3
404 page, Tab order at 390 px (production build, local) A, BUTTON, A, BUTTON per cycle; a button count 2 A, A per cycle; count 0
axe, 28 static public routes × light/dark × 1280/390 (author-run, local) 1 node (light 390), 1 node (dark 390) 0 in all four
The 25 suites naming the changed files — 623/623
Full unit suite / tsc / eslint — 31,619 passed (the 3 failures are this container's Node 22 against the pinned Node 24, red on main too) / 0 / clean

On the same main build, the signed-in app in the dark theme at 390 px (246 routes, empty household) has 0 serious or critical axe findings. The light theme's remaining items are in #998, #1000 and #1001.

No migration, configuration or dependency change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4


Generated by Claude Code

Nine call sites rendered a Button as a Link's child: the 404, error and
in-app not-found pages, the marketing AI page and shared CTA, and two
vacations views. One action had two tab stops (measured on the 404 page:
A, BUTTON, A, BUTTON), interactive content sat inside <a>, and the button
covered its own link: axe's one serious public-page finding, a 9px target
on the 404 page at 390 px in both themes.

ButtonLink is a Next Link carrying Button's own classes; components/ui/
button.tsx is left as it is, and a test holds the copied classes equal to
Button's. A guard refuses a link with a button child anywhere in app/ or
components/, naming the one auth site left for its owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 10, 2026 8:54pm UTC

Request Review

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review October 11, 2026 00:19
@NewWorldVenture
NewWorldVenture merged commit b00b15b into main Oct 11, 2026
9 checks passed
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