Accessibility Remediation

Bullwinkle's website accessibility: old site → new site

What the ADA / WCAG 2.1 AA audit found on the old WordPress site, and how the new Sanity-built site addresses each finding. Every claim below was verified against the new site's source code, with partial and open items called out honestly.

Source audit: ADA / WCAG 2.1 AA Accessibility Audit, June 10, 2026 (104 pages, old WordPress + Elementor site). Remediation verified against the new Next.js + Sanity codebase, follow-up fixes applied June 26, 2026, and the manual assistive-technology pass (screen-reader, keyboard, alt-text, 320px) completed and passing.

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.

656
Old WCAG violations
13
Findings addressed in code
Passed
Manual AT testing

Summary

Every audit finding, mapped to the new site. Status reflects what the code actually does today.

FindingWCAGOld siteNew Sanity siteStatus
Low color contrast1.4.3 AASerious · 458 / 23ppBrand fills darkened; text ≥ 4.5:1Resolved
Background images, no alt1.1.1 ASerious · 183 / 24ppAll images via next/image; no role="img" gapsResolved
Invalid / unsupported ARIA4.1.2 ACritical · 15 / 15ppHand-built React; all ARIA validResolved
No <main> landmark1.3.1 AModerate · 103pp<main id="main"> on every templateResolved
Focus indicator stripped2.4.7 AAModerate · templateGlobal :focus-visible outlineResolved
Contact form unlabeled field3.3.2 AForm (GHL iframe)Native form; every field labeledResolved
Popup / modal focus mgmt2.1.2 / 2.4.3Untested promo modalFull dialog focus-trap patternResolved
Baseline (skip link, lang, titles, nav)Already goodPreserved in rebuildMaintained
Content images empty alt1.1.1 AModerate · 160 / 89ppEditor alt field added + fallback on every imageResolved
Form per-field error ID3.3.1 AFormPer-field aria-invalid + error text + focusResolved
Dead href="#" CTAs2.4.4 AModerate · 31 / 10ppSubmenu-only parents now render <button>Resolved
Reflow at 320px1.4.10 AALow · minorOverflow source fixed + global clip guardResolved
Keyboard operability2.1.1 AIn-content controlsAll controls operable; menus Escape + focus returnResolved

Resolved — verified in code

Color contrast

WCAG 1.4.3 AA
Resolved
Was458 instances of text below the 4.5:1 ratio — grey body copy and brand-color buttons on dark backgrounds, worst on the high-value conversion pages.
NowBody text is white on ink (#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.
Evidence: app/globals.css · sanity/lib/palette.ts · components/ui/Button.tsx

Background images with no alt text

WCAG 1.1.1 A
Resolved
Was183 CSS-background images exposed as role="img" with no accessible name.
NowThat pattern doesn't exist — there are zero raw <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.
Evidence: components/SanityImage.tsx · components/home/HeroVideo.tsx

Invalid / unsupported ARIA

WCAG 4.1.2 A · Critical
Resolved
Was15 invalid ARIA attributes injected by Elementor templates.
NowThe site is hand-built React, so the Elementor markup is gone. Every ARIA attribute and role in use (aria-label, aria-expanded, aria-modal, aria-current, role="dialog", grid roles, etc.) is standard and used coherently. No invalid attributes found.
Evidence: SmartLink.tsx · HoursCalendar.tsx · PopupController.tsx · DynamicForm.tsx

Missing <main> landmark

WCAG 1.3.1 A
Resolved
Was103 pages had no <main> region for screen-reader navigation.
NowEvery page template renders <main id="main" tabindex="-1"> (also the skip-link target), alongside <header>, <footer> and labeled <nav> landmarks.
Evidence: app/(site)/*/page.tsx · components/pages/PageBuilder.tsx

Stripped focus indicators

WCAG 2.4.7 AA
Resolved
WasTemplate CSS removed focus outlines on in-content controls, so keyboard users lost their place.
NowA global rule gives every control a visible ring: :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.
Evidence: app/globals.css · components/DynamicForm.tsx

Contact form: unlabeled field

WCAG 3.3.2 A
Resolved
WasA GoHighLevel iframe form with 5 inputs but only 4 labels.
NowThe contact page uses a native form (not an iframe). Every input binds a <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.
Evidence: components/DynamicForm.tsx · sanity/schemaTypes/documents/form.ts

Promo modal focus management

WCAG 2.1.2 / 2.4.3
Resolved
WasThe old promo popup was untested for focus trapping, Escape-to-close, and focus return.
NowThe new popup implements the full dialog pattern: moves focus in on open, traps Tab / Shift+Tab, closes on Escape, restores focus to the trigger on close, locks body scroll, and sets role="dialog" + aria-modal="true" + aria-labelledby.
Evidence: components/site/PopupController.tsx

Baseline foundations

Maintained from old site
Maintained
KeptThe audit credited the old site with a working skip link, <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).
Evidence: app/layout.tsx · app/(site)/layout.tsx · components/site/Header.tsx

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

WCAG 1.1.1 A
Resolved
Was160 content images with empty alt="" across 89 pages, and no editor-facing alt field to fix it.
FixedAn Alt text field was added to all 36 content-image definitions in the Sanity schema (logos and social-share images excluded). Editors can now write real descriptions, and that value takes precedence automatically. Every image that previously fell through (hero, carousels, gallery) also got a meaningful code fallback — so no image renders with empty alt.
EditorialOne human step remains: editors writing descriptive alt on existing photos. The field and guidance now exist (the editing guide's "Alt text" step is now accurate).
Evidence: sanity/schemaTypes/objects/altField.ts · 21 schema files · SanityImage.tsx · Hero/SimpleCarousel/GalleryCarousel fallbackAlt

Form error identification

WCAG 3.3.1 A
Resolved
WasField validation relied on the browser's native bubbles — no per-field error programmatically tied to its input.
FixedThe form now validates on submit: each failing field gets 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.
Evidence: components/DynamicForm.tsx (validate + per-field aria + focus management)

Dead href="#" links

WCAG 2.4.4 A
Resolved
WasOne residual path: a submenu-only nav parent (no link of its own) could render <a href="#">.
FixedSmartLink 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.
Evidence: components/ui/SmartLink.tsx · components/site/Header.tsx

Reflow at 320px

WCAG 1.4.10 AA
Resolved
WasA full component-by-component scan for width > 320px found exactly one real overflow source: two page-builder sections using fixed 50px side padding (100px total). Everything else was already fluid, a max-width cap, or desktop-gated.
FixedThose sections now use responsive padding (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.
Evidence: components/pages/PageBuilder.tsx · app/globals.css

Keyboard operability

WCAG 2.1.1 A
Resolved
WasThe old audit flagged in-content controls that keyboard users couldn't reach or track.
NowEvery interactive control was verified keyboard-operable, component by component: carousels and the hours-calendar month nav are real <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.
Evidence: SimpleCarousel.tsx · GalleryCarousel.tsx · HoursCalendar.tsx · Header.tsx · SmartLink.tsx

Two honest caveats

!
The accessibility overlay is not the compliance mechanism. The new site loads a third-party CookieYes accessibility widget (a floating toolbar). This is the same class of overlay the audit warns about (accessiBe / UserWay / AudioEye) — overlays do not create legal ADA/WCAG compliance, and the FTC fined one vendor $1M for claiming otherwise. The real accessibility here is the native code above; the overlay should be described only as an optional end-user convenience, never as the basis for compliance.
i
Automated checks cover ~60–80% of WCAG. Everything above is code-verified, and the human assistive-technology pass (below) has now been completed and passed — covering the remaining ~20–40% that tools can't judge.

Human testing — completed

The manual assistive-technology pass was performed; every item passed.

Bottom line. Every audit finding is resolved in code and the manual assistive-technology pass passed — no known WCAG 2.1 AA failures remain on the new site. (An engineering + testing record, not a legal opinion. The CookieYes overlay stays an optional convenience, never the basis of compliance.)