149 lines
5.5 KiB
Markdown
149 lines
5.5 KiB
Markdown
### 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>`):
|
|
```astro
|
|
<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).
|
|
|
|
2) Page language + `hreflang`
|
|
- File: `src/layouts/BaseLayout.astro`
|
|
- Snippet (top `<html>` and `<head>`):
|
|
```astro
|
|
<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).
|
|
|
|
3) Mobile menu button lacks ARIA state wiring
|
|
- File: `src/layouts/BaseLayout.astro`
|
|
- Snippet (button + script):
|
|
```astro
|
|
<button class="md:hidden" id="mobile-menu-button" aria-controls="mobile-menu" aria-expanded="false">
|
|
...
|
|
</button>
|
|
```
|
|
```html
|
|
<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).
|
|
|
|
4) Carousel semantics and controls
|
|
- File: `src/components/HeroCarousel.jsx`
|
|
- Snippet:
|
|
```jsx
|
|
<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).
|
|
|
|
5) Contact form ARIA and honeypot
|
|
- File: `src/components/ContactForm.jsx`
|
|
- Snippet:
|
|
```jsx
|
|
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).
|
|
|
|
6) Lightbox dialog semantics
|
|
- File: `src/components/Lightbox.jsx`
|
|
- Snippet:
|
|
```jsx
|
|
<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).
|
|
|
|
7) 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`
|
|
```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.
|