Repository navigation
Skip link, focus-visible baseline, and a keyboard-navigable header #39
Description
Task
Make the app fully usable by keyboard: a skip-to-content link in the root layout, a visible focus indicator that is not the browser default on every interactive element, and a header whose mobile menu can be opened, navigated and closed without a mouse.
Context
app/layout.tsx:15-25renders<html><body>→ a testnet banner div →<SiteHeader />→{children}. No skip link exists (I grepped).components/nav/SiteHeader.tsxis"use client"(:1), has the hamburger with anaria-label(:96), and is the only chrome on every page.app/claim/[id]/ClaimView.tsx:163-166,182is the one place in the app with focus management — the pattern to copy, but the file is out of scope.- Form labels are in good shape already (
app/send/SendForm.tsx:49,87,126usehtmlFor;app/verify/VerifyForm.tsx:15wraps). This issue is about focus and traversal order, not labels. - Reduced-motion is handled (
app/globals.css:48,78,motion-reduce:on the header) — keep it working.
Before you start: mandatory context review
Kinlock moves other people's money, so the docs are binding constraints, not background reading.
They record decisions this task must not silently reopen. Read them in this order and tick each box:
-
docs/ARCHITECTURE_ESSENTIALS.md— start here, it is short (AGENTS.md§1) -
docs/ARCHITECTURE.md— only the sections named in Context above -
docs/PRD.md— if this task touches user-facing behaviour or scope -
AGENTS.md§3 (hard rules), §4 (deferred list), §8 for this repo, §12 (roadmap rule) -
ROADMAP.md— the row(s) listed under Related roadmap rows, including their Depends on
In your first PR comment, name the hard-rule numbers or doc sections that constrain this task. That
line is how we know the review happened.
Then:
- If code and docs disagree, or two docs disagree: stop and say so on this issue. Do not pick one
silently, and do not "fix" either side without approval (AGENTS.md§1). - If the task looks like it needs anything from the deferred list (§4), a change to a hard rule (§3),
or a new dependency (§2): raise it here first. Those need a human; deciding them is not your job. - If a row you are told to finish is
BLOCKEDor depends on a row that is notDONE, say so here
instead of starting.
Why we need this
AGENTS.md§8.3: "Accessibility: keyboard navigation, labels, contrast. Treat as required, not polish."- A sender on a phone with a Bluetooth keyboard, a screen-reader user, and anyone who lost their mouse all need to reach the form without tabbing through the whole nav twice.
- The banner above the header is currently the first tab stop on every page, which is exactly the thing a skip link exists to escape.
Scope
In scope
app/layout.tsxand a newcomponents/a11y/SkipLink.tsxcomponents/nav/SiteHeader.tsxand its menuapp/globals.cssfor the focus-visible stylesmessages/en.jsonfor the skip-link label- A test that the skip link is the first focusable element
Do not touch (security-sensitive or generated; changing these needs a human first — AGENTS.md §2, docs/project_structure.md §8)
app/claim/**andlib/claim-links/**— the claim-link fragment (#r=…&s=…) must never reach a server, log or analytics tool (hard rule 5)next.config.ts,proxy.ts/middleware.ts, CSP headerslib/wallet/**and any direct contract call site — money-moving calls go through the SDK and the wallet adapter onlyapp/send/**,app/locks/**,app/payee/**action logic — release, refund and decline paths stay with maintainerspackage.json,pnpm-lock.yaml(no new dependencies without approval),.github/workflows/**app/claim/**(including copying its focus pattern into it)", "adding an a11y lint plugin — new dependency, ask first
Suggested implementation
- Read the app rules in
AGENTS.md§8.3 and the WCAG 2.2 focus docs before coding; get the pattern right once inSiteHeader, then apply it. - Skip link: visually hidden until focused, then the first visible focus ring on the page, targeting the main content container. Ensure the target gets
tabIndex={-1}so focus actually moves. - Give every interactive element a focus ring that survives both colour schemes (see the contrast issue in this repo — the muted token work may help; sequence accordingly or coordinate on the issue).
- Menu:
Enter/Spacetoggle,Escapecloses, focus returns to the toggle, and auseEffectcleanup for the listener. Keep it simple — no focus-trap library. - Verify in a real browser: tab through
/,/send,/verify,/payeeand record where you land. Say what you did. - Do not add a third-party script for testing:
AGENTS.md§8.3 bars them on/claim, and the fragment must never reach a server either way (hard rule 5).
A different approach is usually welcome — post it here before building and we will answer. This is a
suggestion, not a spec.
Edge cases and traps
- The testnet banner renders before
SiteHeaderinapp/layout.tsx:15-25, so it is the current first tab stop. A skip link placed after it does nothing. - The target needs
tabIndex={-1}or focus stays on the skip link and the screen reader keeps reading the nav. Escapemust close the menu and return focus to the toggle, with the document listener cleaned up on unmount.- Focus ring must clear 3:1 against adjacent colours in both schemes, and
motion-reduce:behaviour on the header must keep working. - Another open issue in this repo touches
SiteHeader.tsxand the tokens. Say on this issue which one you took first so the two do not collide.
Done means
- First tab stop on every in-scope page is the skip link, and it moves focus to the content
- Focus is visible at ≥ 3:1 against adjacent colours in light and dark
- The mobile menu opens, traverses and closes by keyboard alone, with focus restored
- No tab traps and no invisible-but-focusable elements in the header
- A test asserts the skip link renders first;
pnpm lint/typecheck/test/buildpass - You tabbed through the four in-scope routes in a browser and reported the result
Tests
One test for render order. If you need DOM rendering, @testing-library/react is a new dev dependency — ask on the issue; otherwise keep the assertion at the layout-markup level, which the existing node-environment vitest can do.
Documentation
No API change. Mention the focus convention in the component so the pattern is not reinvented per page.
Verify before you open the PR
pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build
Run every command above and paste the real output in the PR. We do not accept "should pass" —
AGENTS.md §6: "You actually ran the commands."
Opening the pull request
- Assignment is required before you start. Comment on this issue and wait to be assigned. Do not
open a PR against an issue someone already holds — if it has been quiet, ask us to release it first. Closes #39in the PR description, so the issue and the PR link automatically.- Branch
feat/…,fix/…,docs/…,test/…orchore/…. Commit subject example:
feat(app): skip link, focus-visible baseline and keyboard-navigable header - Changelog entry;
M3-17staysIN PROGRESS. - Follow
.github/PULL_REQUEST_TEMPLATE.md(AGENTS.md§7). In "Hard rules / invariants touched",
give numbers, not prose. - Small and focused: one logical change. Do not mix a refactor into it.
- Do not add the
security-sensitivelabel — that is the maintainer's flag for contract-logic PRs. - Do not reformat or touch files outside Scope. Unrelated diff noise costs review time and can
break the roadmap merge. - Never commit to
main; open a PR. Never force-push. - Evidence for the PR: Attach screenshots of the skip link in its focused state and the header menu opened by keyboard, light and dark, plus your tab-order notes.
How we review
Two maintainers: one reviews correctness against the hard rules and the roadmap row's Done when, the
other reviews diff scope and whether the tests were actually run. Order is hard-rule correctness → tests
really executed → diff stays inside Scope → style last. First response on a PR: 2 business days.
What fails review fastest: a change outside Scope, a deleted or weakened assertion, a claim with no
command output behind it, and a ROADMAP.md with no Changelog entry (AGENTS.md §12).
For this issue, look first at the first tab stop on each page and that focus actually moves to the content target, not just to a link.
Tier and effort
tier/medium · roughly 1–2 days. Wave complexity tag: Medium = 150 points — the published mapping is Trivial 100 · Medium 150 · High 200
(Drips' maintainer guide); the cycle's
own values are confirmed on drips.network/wave/stellar before
the Wave opens. Kinlock's tier rules and what each tier means for review:
docs/wave-issue-format.md.
Related roadmap rows
M3-17 — Accessibility pass (keyboard, labels, contrast) (IN PROGRESS)
Part of M3-17; leave it IN PROGRESS and list what remains.
Questions
Comment on this issue — a maintainer answers within 2 business days. Say "I'll take this" so we can flag it on
the Wave board and stop two people doing the same work. If you get half way and stall, post what you
have as a draft PR rather than disappearing; a blocked draft is more useful to us than silence.
Activity
jdjioe5-cpu commentedon Oct 17, 2026
I'll take this issue. Following up on #38 (where semantic tokens were added in PR #45), I will implement the accessible navigation pass: adding \components/a11y/SkipLink.tsx\ rendered first in \�pp/layout.tsx, updating \components/nav/SiteHeader.tsx\ with full keyboard interactions (Enter/Space toggle, Escape to close with focus restoration), \ocus-visible\ styling in \�pp/globals.css, \messages/en.json\ label, and automated test coverage.
jdjioe5-cpu commentedon Oct 17, 2026
Submitted PR #46 addressing this issue.
Summary of Delivery:
components/a11y/SkipLink.tsxplaced as the first element in<body>before the testnet banner and<SiteHeader />. Uses localized stringcommon.skipToContent. Visually hidden with.sr-onlyand styled with high-visibility focused pill.<main id="main-content" tabIndex={-1} className="outline-none">, ensuring focus actually moves into main content upon skip link activation.components/nav/SiteHeader.tsx, addedtoggleRef. PressingEscapedismisses the mobile menu and restores focus back to the toggle button. Addedfocus-visible:outline ring classes to all interactive controls.:focus-visiblerules inapp/globals.cssusing 2px solid--accent(contrast 5.14:1 light, 6.84:1 dark >= 3:1).components/a11y/SkipLink.test.ts(7 tests) covering all DOM ordering, landmarks, focus restoration, and styles.pnpm lint(85 files clean),pnpm typecheck(clean),pnpm test(175/175 passed across 16 files), andpnpm build(clean).