5.5 KiB
5.5 KiB
Omoluabi Accessibility Audit (WCAG 2.2 AA)
Scope: Current Astro site as of this commit. Focus on perceivable, operable, understandable, and robust criteria for Nigerians in NL and Dutch partners.
Executive summary
- Overall: Solid semantic base with clear landmarks in
src/layouts/BaseLayout.astro
. Reduced-motion support exists insrc/styles/global.css
. Key gaps: language/i18n markup, skip links, menu ARIA, carousel semantics, form error/ARIA, dialog semantics, and strict CSP readiness. - Risk: Medium → High for navigation/keyboard and language; Low → Medium for contrast on image/gradient backgrounds.
High-priority issues (fix now)
- Missing skip link + focus target
- File:
src/layouts/BaseLayout.astro
- Snippet (add just inside
<body>
and setid
on<main>
):
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:text-black focus:px-3 focus:py-2 focus:rounded">
Skip to main content
</a>
...
<main id="main" tabindex="-1">
<slot />
</main>
- Rationale: Allows keyboard users to bypass repeated navigation (WCAG 2.4.1, 2.4.3).
- Page language +
hreflang
- File:
src/layouts/BaseLayout.astro
- Snippet (top
<html>
and<head>
):
<html lang={Astro.props.lang ?? 'en'}>
<head>
...
<link rel="alternate" hrefLang="en" href="/en/" />
<link rel="alternate" hrefLang="nl" href="/nl/" />
<link rel="alternate" hrefLang="x-default" href="/" />
- Rationale: Correct language for screen readers and localized SEO (WCAG 3.1.1, 3.1.2).
- Mobile menu button lacks ARIA state wiring
- File:
src/layouts/BaseLayout.astro
- Snippet (button + script):
<button class="md:hidden" id="mobile-menu-button" aria-controls="mobile-menu" aria-expanded="false">
...
</button>
<script>
document.addEventListener('DOMContentLoaded', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', () => {
const isHidden = mobileMenu.classList.toggle('hidden');
mobileMenuButton.setAttribute('aria-expanded', String(!isHidden));
});
}
});
</script>
- Rationale: Communicates expanded/collapsed state for assistive tech (WCAG 4.1.2).
- Carousel semantics and controls
- File:
src/components/HeroCarousel.jsx
- Snippet:
<div
className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white"
role="region"
aria-roledescription="carousel"
aria-label="Featured content"
>
{slides.map((slide, index) => (
<div
key={index}
role="group"
aria-roledescription="slide"
aria-label={`${index + 1} of ${slides.length}`}
className={...}
>
...
</div>
))}
<button aria-pressed={!isPlaying} aria-label={isPlaying ? 'Pause slideshow' : 'Play slideshow'}>...</button>
</div>
- Rationale: Conveys carousel/slide semantics; exposes play/pause state (WCAG 1.4.2, 2.2.2, 4.1.2).
- Contact form ARIA and honeypot
- File:
src/components/ContactForm.jsx
- Snippet:
const [hp, setHp] = useState('');
...
<form onSubmit={handleSubmit} noValidate ...>
<div className="hidden" aria-hidden="true">
<label htmlFor="website">Website</label>
<input id="website" name="website" value={hp} onChange={(e)=>setHp(e.target.value)} tabIndex={-1} autoComplete="off" />
</div>
<input aria-invalid={Boolean(errors.name)} aria-describedby={errors.name ? 'name-error' : undefined} ... />
{errors.name && <p id="name-error" className="...">{errors.name}</p>}
- Rationale: Accessible errors association and simple anti-bot (WCAG 3.3.1/3.3.3).
- Lightbox dialog semantics
- File:
src/components/Lightbox.jsx
- Snippet:
<div
className="fixed inset-0 ..."
role="dialog"
aria-modal="true"
aria-label="Image viewer"
onClick={onClose}
>
- Rationale: Dialog semantics and modality (WCAG 1.3.1, 2.4.3, 4.1.2).
- Inline styles and data-URI backgrounds limit strict CSP
- Files:
src/pages/about.astro
,src/pages/donate.astro
,src/pages/orphanage.astro
- Fix: Extract inline
style="background: ..."
to named CSS classes insrc/styles/main.css
(e.g.,.bg-flag-nl-ng
,.pattern-overlay
) and reference viaclass
only. Move inline<script>
in layout to a module file and import withtype="module"
andnonce
. - Rationale: Enables strict non-
unsafe-inline
CSP (security and robustness).
Medium-priority issues
- Contrast over imagery/gradients: Ensure minimum overlay like
bg-black/60
behind text; avoid fully transparent gradient text for body copy. Files:src/pages/index.astro
,src/pages/about.astro
,HeroCarousel.jsx
. - Focus visibility baseline: Add base
:focus-visible
style insrc/styles/global.css
to ensure consistent visible focus. - Image dimensions: Add
width
/height
on<img>
where known to prevent CLS.
Suggested global utilities
- File:
src/styles/global.css
:focus-visible { outline: 2px solid var(--nigerian-green); outline-offset: 2px; }
[role="button"], a, button, input, select, textarea { -webkit-tap-highlight-color: transparent; }
Verification plan (to add in CI)
- Playwright +
@axe-core/playwright
: nav landmarks, skip link focus, keyboard-only menu, form labels/errors, dialog semantics. - Lighthouse budgets: LCP < 2.0s, CLS < 0.05, TTI < 3.5s; run on PR and fail if exceeded.
- Spot-check color contrast with axe; prohibit text over images without overlay.