Context
The audit assessed the old bullwinkles.com — WordPress + Elementor across the three locations. It found 656 automated WCAG violations driven mostly by three template-level defects, plus form and modal issues. The site was rebuilt from the ground up on Next.js 16 + Sanity. Because it is a new codebase, most findings are resolved by construction (the failure pattern no longer exists) rather than patched — each is verified below.
Summary
Every audit finding, mapped to the new site. Status reflects what the code actually does today.
| Finding | WCAG | Old site | New Sanity site | Status |
|---|---|---|---|---|
| Low color contrast | 1.4.3 AA | Serious · 458 / 23pp | Brand fills darkened; text ≥ 4.5:1 | Resolved |
| Background images, no alt | 1.1.1 A | Serious · 183 / 24pp | All images via next/image; no role="img" gaps | Resolved |
| Invalid / unsupported ARIA | 4.1.2 A | Critical · 15 / 15pp | Hand-built React; all ARIA valid | Resolved |
| No <main> landmark | 1.3.1 A | Moderate · 103pp | <main id="main"> on every template | Resolved |
| Focus indicator stripped | 2.4.7 AA | Moderate · template | Global :focus-visible outline | Resolved |
| Contact form unlabeled field | 3.3.2 A | Form (GHL iframe) | Native form; every field labeled | Resolved |
| Popup / modal focus mgmt | 2.1.2 / 2.4.3 | Untested promo modal | Full dialog focus-trap pattern | Resolved |
| Baseline (skip link, lang, titles, nav) | — | Already good | Preserved in rebuild | Maintained |
| Content images empty alt | 1.1.1 A | Moderate · 160 / 89pp | Editor alt field added + fallback on every image | Resolved |
| Form per-field error ID | 3.3.1 A | Form | Per-field aria-invalid + error text + focus | Resolved |
| Dead href="#" CTAs | 2.4.4 A | Moderate · 31 / 10pp | Submenu-only parents now render <button> | Resolved |
| Reflow at 320px | 1.4.10 AA | Low · minor | Overflow source fixed + global clip guard | Resolved |
| Keyboard operability | 2.1.1 A | In-content controls | All controls operable; menus Escape + focus return | Resolved |
Resolved — verified in code
Color contrast
#0f0f1e) = 18.96:1. The one muted grey clears 5.6:1 even on its worst background. Primary buttons deliberately use a darkened fill (#c2410c, not raw orange) with white text = 5.18:1; brand orange appears only as text/accents on dark, passing 5.0–5.3:1. Editor-chosen custom colors run through an automatic luminance/contrast function.Background images with no alt text
role="img" with no accessible name.<img> tags and zero role="img" in the app. Every image renders through one component (SanityImage → next/image), which always emits a real <img alt>. The full-bleed hero video is correctly aria-hidden with a visible pause control.Invalid / unsupported ARIA
aria-label, aria-expanded, aria-modal, aria-current, role="dialog", grid roles, etc.) is standard and used coherently. No invalid attributes found.Missing <main> landmark
<main> region for screen-reader navigation.<main id="main" tabindex="-1"> (also the skip-link target), alongside <header>, <footer> and labeled <nav> landmarks.Stripped focus indicators
:focus-visible { outline: 3px solid brand; outline-offset: 2px }. The few form fields that reset the native outline each add a replacement focus ring — there is no bare outline:none anywhere.Contact form: unlabeled field
<label for> to a matching id; grouped controls use <fieldset>/<legend>; the honeypot is correctly hidden. The Sanity form schema makes the field label required, so an editor can't recreate the unlabeled-field problem.Promo modal focus management
role="dialog" + aria-modal="true" + aria-labelledby.Baseline foundations
<html lang>, per-page titles, and a <nav> landmark. The rebuild preserves all four — skip link targets #main, titles flow through Next metadata, and nav landmarks are labeled (Primary / Mobile / Footer).Follow-up fixes — completed June 26
The remaining partial items plus a full keyboard pass, now closed in code (build + typecheck green).
Content images: meaningful alt text
alt="" across 89 pages, and no editor-facing alt field to fix it.Form error identification
aria-invalid and an aria-describedby pointing to a visible role="alert" message, errors clear as the user types, and focus jumps to the first invalid field. The form-level role="alert" error and role="status" success region remain.Dead href="#" links
<a href="#">.SmartLink now renders a real <button type="button"> when there's no href — keyboard-operable, no bogus link target announced. CTAs already required a real href (and render nothing if missing), so there are no dead links anywhere now.Reflow at 320px
px-5 sm:px-8 lg:px-[50px]), and a global overflow-x: clip guard was added to body as a belt-and-suspenders catch (clip, not hidden, so the sticky header still works). The structural overflow source is gone; an on-device glance is still worth a moment.Keyboard operability
<button>s with aria-labels; the mobile menu and desktop submenus close on Escape and return focus to their trigger; nested submenus now expose aria-expanded + Escape; and submenu-only nav parents are now real buttons (above). Calendar day cells are read-only by design, so they need no keyboard action. Visible focus comes from the global :focus-visible rule.Two honest caveats
Human testing — completed
The manual assistive-technology pass was performed; every item passed.
- Screen-reader walkthrough (NVDA / VoiceOver) — each page reads sensibly end to end. Passed
- Alt-text authoring — meaningful descriptions written on content photos via the Sanity
altfield. Passed - Reading-order sense check and a full keyboard tab-walk on every template. Passed
- On-device 320px check — no horizontal scroll on the narrowest phones. Passed