feat(a11y): focus states, skip link and landmark names for header/nav (closes #14) - #17
Open
dchaudhari7177 wants to merge 1 commit into
Open
Conversation
The scaffold styled :hover and nothing else, so tabbing through the
header was invisible - the links were reachable but you could not tell
where you were. That is the actual barrier here; the links themselves
were already real anchors.
- globals.css: one :focus-visible ring for every interactive control,
rather than each component remembering its own. :focus-visible not
:focus, so a mouse click leaves no ring, and a 2px offset so the ring
clears the pill links' own border.
- site-header.tsx: a skip link as the first focusable element, hidden
until focused (WCAG 2.4.1). Kept as a plain <a>, not next/link - a
same-page fragment should not go through the router.
- page-scaffold.tsx: <main id="main-content" tabIndex={-1}> so the skip
link lands somewhere and moves focus, not just scroll position.
- The header nav is aria-label="Primary" rather than "Global", since
SectionNav is also on the page and "Global" does not distinguish them.
- The wordmark gets an accessible name saying it goes home; its visible
text is only the site name.
- main:focus outline is suppressed: focusing a landmark moves the
reading position, it is not a control to highlight.
No visual change for mouse users; nothing about the Figma layout moves.
Tests: 6 in site-header.test.tsx - banner landmark, named nav landmark,
the wordmark's accessible name, the skip link being first in tab order
and pointing at #main-content, the full tab order through every primary
link, and every nav destination being a real href.
Verified they can fail: removing the skip link and the nav label fails 4
of the 6.
Gate: npm run lint, npm run typecheck, npm run test:run (15 passed, 9
before) and npm run build all pass - i.e. the full `check` script.
Read node_modules/next/dist/docs per AGENTS.md before changing anything;
the Link reference confirms <Link> renders an <a> and that hash hrefs
are handled, which is why the skip link stays a plain anchor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@dchaudhari7177 is attempting to deploy a commit to the Heymide Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14.
The scaffold styled
:hoverand nothing else, so tabbing through the header was invisible — the links were reachable, you just couldn't tell where you were. That's the actual barrier; the links themselves were already real anchors.Changes
globals.css:focus-visiblering for every interactive controlsite-header.tsxaria-label="Primary"on the nav, accessible name on the wordmarkpage-scaffold.tsx<main id="main-content" tabIndex={-1}>Focus ring is central, not per-component. A
:where(a, button, input, …):focus-visiblerule means the next contributor doesn't have to remember to add one.:focus-visiblerather than:focusso a mouse click leaves no ring behind, and a 2px offset so the ring clears the pill links' own border — on those, the border is the edge, and a flush ring reads as a thicker border rather than as focus.The skip link is the first focusable element and is hidden until focused (WCAG 2.4.1, Bypass Blocks). It's a plain
<a>, deliberately notnext/link: a same-page fragment shouldn't go through the router.tabIndex={-1}on<main>is the part that's easy to miss — without it the browser scrolls to the target but leaves focus in the header, so the next Tab drops you back into the nav you just skipped.main:focus { outline: none }because focusing a landmark moves the reading position; it isn't a control to highlight.aria-label="Primary", not"Global"—SectionNav(aria-label="Section routes") is on the same page, and "Global" doesn't distinguish the two in a landmark list.No visual change for mouse users, and nothing about the Figma layout moves.
Tests
6 in
site-header.test.tsx: banner landmark, named navigation landmark, the wordmark's accessible name, the skip link being first in tab order and pointing at#main-content, the complete tab order through every primary link, and every nav destination being a realhrefrather than a click handler.Verified they can fail — removing the skip link and the nav label fails 4 of the 6:
Gate
The full
checkscript, all four steps:npm run lint— cleannpm run typecheck(next typegen && tsc --noEmit) — cleannpm run test:run— 9 → 15 passed, statements 96.29%npm run build— succeedsPer
AGENTS.mdI read the bundlednode_modules/next/dist/docs(Next 16.2.6) before writing: theLinkreference confirms<Link>renders an<a>and that hashhrefs are handled, which is what settled the skip link staying a plain anchor.Not covered
Contrast ratios against the Figma palette. The ring uses
--color-accent, so if #15 retunes the brand tokens the focus ring follows automatically — but someone with Figma access should sanity-check the ring against the final dark palette.🤖 Generated with Claude Code