fix(a11y): visible focus indicator, aria-expanded, and six Label-in-Name failures

Batch 10, and only three of its six issues were real. The other three are
answered on the tracker with the arithmetic.

#189 — focus indicator. Footer and Home carried zero focus styling across 17
links. Added one global :focus-visible rule rather than sprinkling classes,
because the issue's own scope was "all interactive <a> and <Link> elements".

Not the ring the issue asked for. It suggested ring-primary-cyan; #22D3EE is
6.26:1 on navy and 2.38:1 on white, so on this light-first design that ring
would have failed WCAG 1.4.11 across most of the site. Two rings instead — white
inside, navy outside — so the white carries the dark bands and the navy carries
the light sections. Worst case across every background in the palette is
13.62:1. box-shadow so both rings follow each element's own border-radius, plus
a transparent outline for forced-colors mode.

#192 — aria-expanded={isOpen} and aria-controls on the mobile nav trigger, and
the id on SheetContent it now points at. Confirmed as "false" in the
prerendered HTML.

#193 — INVERTED. The issue asked for aria-labels to be added consistently to
hero CTAs. The aria-labels already there were WCAG 2.5.3 Label in Name failures,
Level A: "Schedule a consultation" is not a superset of the visible "Schedule
Consultation", so a voice-control user saying what they see cannot activate the
link. Doing what the issue asked would have spread a Level A failure. Removed
the six that broke it; kept the eight that genuinely add context and do contain
their visible text, including the icon-only header logo link that needs one.

Audit now reports 0 failures across Home, Services, Footer and Header.

Verified: npm run build; the ring is in the built CSS, aria-expanded="false" and
id="mobile-nav-content" are in the prerendered HTML, all six removed labels are
absent from it, and the visible CTA text still renders.

NOT verified: nothing has been walked keyboard-only in a browser, and no screen
reader has been used. That gap is recorded in docs/qa/ClaudeQACoverage.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scarlett 2026-08-18 02:31:13 -05:00
parent 615043db44
commit 9fdc6b6032
5 changed files with 41 additions and 8 deletions

View File

@ -67,7 +67,7 @@ const Footer = () => {
<Link
to="/contact#contact-form"
className="inline-flex items-center gap-2 rounded-md text-sm font-semibold px-5 py-2.5 bg-primary-cyan text-primary-navy hover:bg-white transition-colors duration-200"
aria-label="Request a free consultation"
>
Get a Free Quote
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>

View File

@ -142,7 +142,7 @@ const Header = () => {
{/* CTA Button */}
<div className="hidden md:block">
<Link to="/contact#contact-form" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 bg-primary-cyan text-primary-navy hover:bg-cyan-600 transition-colors" aria-label="Request a consultation">
<Link to="/contact#contact-form" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-3 bg-primary-cyan text-primary-navy hover:bg-cyan-600 transition-colors">
Request Consultation
</Link>
</div>

View File

@ -40,7 +40,12 @@ const MobileNav = () => {
<div className="md:hidden">
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<button className="p-2 text-white hover:text-primary-cyan focus:outline-none focus:ring-2 focus:ring-primary-cyan rounded-md" aria-label="Open navigation menu">
<button
className="p-2 text-white hover:text-primary-cyan rounded-md"
aria-label="Open navigation menu"
aria-expanded={isOpen}
aria-controls="mobile-nav-content"
>
<svg
className="h-6 w-6"
fill="none"
@ -57,7 +62,7 @@ const MobileNav = () => {
<span className="sr-only">Open menu</span>
</button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px] sm:w-[350px] bg-primary-navy text-white">
<SheetContent id="mobile-nav-content" side="right" className="w-[300px] sm:w-[350px] bg-primary-navy text-white">
<div className="flex flex-col h-full">
<div className="flex items-center gap-3 mb-6">
<img

View File

@ -75,3 +75,31 @@ a:hover {
.section-alt {
background: #EEF6FB;
}
@layer base {
/*
* Visible keyboard focus WCAG 2.4.7 (Focus Visible) and 1.4.11 (Non-text
* Contrast, which wants 3:1 for the indicator itself).
*
* Two rings, not one, and that is the whole point. This is a light-first
* design with dark navy bands through it, so no single colour clears 3:1
* everywhere: primary-cyan manages 6.26:1 on navy and only 2.38:1 on white,
* which is why the obvious `ring-primary-cyan` was not used. The white ring
* carries the dark sections and the navy ring carries the light ones, and on
* every background in the palette at least one of them clears 3:1 by a wide
* margin the worst case is 13.62:1.
*
* box-shadow rather than outline so both rings stack predictably and follow
* each element's own border-radius. The transparent outline is the standard
* escape hatch for Windows High Contrast / forced-colors mode, where
* box-shadow is discarded and an outline is not.
*
* :focus-visible, not :focus, so a mouse click does not leave a ring behind.
* :where() keeps specificity at zero, so any component can still override it.
*/
:where(a, button, [role="button"], summary, input, select, textarea):focus-visible {
box-shadow: 0 0 0 2px #FFFFFF, 0 0 0 5px #0B2A3C;
outline: 2px solid transparent;
outline-offset: 2px;
}
}

View File

@ -125,11 +125,11 @@ const Home = () => {
Business phone, contact center, network, and IT support built around one accountable implementation partner.
</p>
<div className="mt-8 flex flex-col sm:flex-row gap-3">
<Link to="/contact#contact-form" className="inline-flex w-full sm:w-auto h-11 items-center justify-center gap-2 rounded-md bg-white px-5 text-sm font-semibold text-primary-navy hover:bg-section-alt transition-colors" aria-label="Schedule a consultation">
<Link to="/contact#contact-form" className="inline-flex w-full sm:w-auto h-11 items-center justify-center gap-2 rounded-md bg-white px-5 text-sm font-semibold text-primary-navy hover:bg-section-alt transition-colors">
Schedule Consultation
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
<Link to="/services" className="inline-flex w-full sm:w-auto h-11 items-center justify-center rounded-md border border-white/45 px-5 text-sm font-semibold text-white hover:bg-white/10 transition-colors" aria-label="View our services">
<Link to="/services" className="inline-flex w-full sm:w-auto h-11 items-center justify-center rounded-md border border-white/45 px-5 text-sm font-semibold text-white hover:bg-white/10 transition-colors">
View Services
</Link>
</div>
@ -241,7 +241,7 @@ const Home = () => {
Four concrete differentiators that set us apart
</p>
<div>
<Link to="/contact#contact-form" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors" aria-label="Request a consultation">
<Link to="/contact#contact-form" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
Request Consultation
</Link>
</div>
@ -372,7 +372,7 @@ const Home = () => {
<p className="text-lg text-soft-text mb-8 max-w-2xl mx-auto">
Share a few details and we'll provide clear direction.
</p>
<Link to="/contact#contact-form" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors" aria-label="Request a consultation">
<Link to="/contact#contact-form" className="inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-6 bg-primary-navy text-white hover:bg-primary-navy-dark transition-colors">
Request Consultation
</Link>
</div>