Files
Omoluabi/ACCESSIBILITY_AUDIT.md

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 in src/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)

  1. Missing skip link + focus target
  • File: src/layouts/BaseLayout.astro
  • Snippet (add just inside <body> and set id 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).
  1. 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).
  1. 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).
  1. 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).
  1. 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).
  1. 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).
  1. 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 in src/styles/main.css (e.g., .bg-flag-nl-ng, .pattern-overlay) and reference via class only. Move inline <script> in layout to a module file and import with type="module" and nonce.
  • 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 in src/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.