WCAG 2.1 AA Conformiteit voor Onderwijswebsites: Next.js Checklist
WCAG 2.1 AA-compatibiliteit voor onderwijswebsites: Next.js controlelijst
In 2024 alleen al zijn meer dan 200 ADA-gerelateerde rechtszaken gericht tegen onderwijsinstellingen voor ontoegankelijke websites. Settlements varieerden van $25K tot $300K+, en dat is voordat de hersteelkosten erbij komen. Uw universiteits- of schooldistrictwebsite is een juridisch doelwit als deze niet voldoet aan de WCAG 2.1 AA-normen.
Dit is wat de meeste bureaus je niet zullen vertellen: dezelfde technologie die je website sneller maakt, maakt deze ook toegankelijker. Een Next.js-site gebouwd met semantische HTML en Tailwind CSS haalt Lighthouse-toegankelijkheidsscores van 95+ uit de box. Een typische WordPress-site met 30 plugins? Die land ergens tussen 40 en 60. Ik heb genoeg onderwijswebsites geaudit om te weten dat de platformkeuze die je vandaag maakt bepaalt of je volgende jaar hersteeltickets indient of rustig slaapt.
Dit is geen theoretisch overzicht. Het is een werkende technische controlelijst -- acht categorieën, echte codevoorbeelden, en de exacte implementatiepatronen die we gebruiken bij het bouwen van toegankelijke onderwijssites bij Social Animal. Voeg het toe aan bladwijzers. Deel het met je dev-team. Print het uit en plak het aan de muur.
Inhoudsopgave
- De juridische context: waarom onderwijswebsites doelwitten zijn
- 1. Semantische HTML
- 2. ARIA-labels en live-regio's
- 3. Toetsenbordnavigatie
- 4. Kleurcontrast
- 5. Alt-tekst voor afbeeldingen
- 6. Toegankelijke formulieren
- 7. Video en multimedia
- 8. Geautomatiseerde testen in CI/CD
- Waarom WordPress en Drupal worstelen met toegankelijkheid
- Het headless-voordeel: toegankelijkheid afgedwongen bij build-tijd
- Veelgestelde vragen

De juridische context: waarom onderwijswebsites doelwitten zijn
Laten we eerst de juridische zaken afhandelen, want dit is wat je CFO aan het betalen zet.
Sectie 508 van de Rehabilitation Act is van toepassing op alle federale agentschappen en elke instelling die federale financiering ontvangt. Dat is elke openbare universiteit. Elk openbaar schooldistrict. Als uw instelling een enkel dollar federale gelden ontvangt -- inclusief Pell Grants, onderzoeksfinanciering of Title I-gelden -- Sectie 508 is op u van toepassing.
ADA Titel III dekt plaats van openbare dienstverlening. Rechters hebben consistent bepaald dat dit particuliere universiteiten en hun websites omvat. Harvard, MIT en talloze kleinere particuliere instellingen hebben rechtszaken onder Titel III aangespannen.
WCAG 2.1 AA is de technische norm waarnaar rechters verwijzen bij evaluatie van compliance. Het DOJ gaf in 2024 een definitieve regel uit waarin expliciet staat dat staatse en lokale regering websites (inclusief openbare universiteiten en schooldistricten) in overeenstemming moeten zijn met WCAG 2.1 Level AA. Dit is geen suggestie. Het is een regel met handhavingstermijnen.
De cijfers vertellen het verhaal: ADA-rechtszaken tegen onderwijsinstellingen zijn tussen 2018 en 2025 met ongeveer 300% gestegen. Het Office for Civil Rights (OCR) loste in FY2024 meer dan 15.000 klachten op, waarbij digitale toegankelijkheid een van de snelst groeiende klachtcategorieën is.
| Juridisch kader | Van toepassing op | Norm | Belangrijke termijn |
|---|---|---|---|
| Sectie 508 | Openbare universiteiten, schooldistricten (federale financieringsontvangers) | WCAG 2.1 AA | Reeds afdwingbaar |
| ADA Titel II (DOJ 2024-regel) | Staatsorganen/lokale overheidsinstellingen | WCAG 2.1 AA | April 2026 (grote entiteiten), April 2027 (kleine) |
| ADA Titel III | Particuliere universiteiten, particuliere K-12-scholen | WCAG 2.1 AA (de facto) | Rechters handhaven nu |
| Staatswetten (CA, NY, enz.) | Varieert per staat | Typisch WCAG 2.1 AA | Varieert |
Laten we nu een website bouwen die daadwerkelijk slaagt.
1. Semantische HTML
Semantische HTML is de basis van alles. Als je dit verkeerd doet, zal geen hoeveelheid ARIA-attributen je redden. Schermlezers vertrouwen op de semantische structuur van het document om gebruikers te helpen de paginahiërarchie te begrijpen en tussen secties te navigeren.
Koppelingshiërarchie
Elke pagina krijgt precies één <h1>. Subkoppelingen volgen in volgorde: <h2>, dan <h3>, dan <h4>. Sla nooit niveaus over. Een gebruiker van een schermlezer die "Koppelingsniveau 4" hoort na "Koppelingsniveau 2" verliest context.
Ik zie onderwijssites deze regel voortdurend breken -- vooral op afdelingspagina's waar iemand in een CMS inhoud plakte met willekeurige koppelingsniveaus omdat de lettergrootte er goed uitzag visueel.
Landmarkselementen
Gebruik <nav>, <main>, <aside>, <footer> en <header>. Deze creëren navigeerbare regio's voor gebruikers van schermlezers. Een JAWS- of NVDA-gebruiker kan een enkele toets indrukken om tussen landmarks te springen.
Knoppen versus koppelingen
Dit drijft me echt tot razernij. Gebruik <button> voor interactieve elementen die een actie uitvoeren (menu openen, formulier indienen, filter schakelen). Gebruik <a> voor navigatie die de gebruiker naar een nieuwe pagina of sectie brengt. Gebruik nooit een <div> met een onClick-handler.
// ❌ Fout: div doet alsof het een knop is
<div onClick={handleClick} className="cursor-pointer">
Menu openen
</div>
// ❌ Fout: knop gebruikt voor navigatie
<button onClick={() => router.push('/admissions')}>
Toelatingsinformatie bekijken
</button>
// ✅ Correct: semantische knop voor acties
<button
onClick={handleMenuToggle}
aria-expanded={isOpen}
aria-controls="main-nav"
className="focus-visible:ring-2 focus-visible:ring-offset-2"
>
Menu openen
</button>
// ✅ Correct: anker voor navigatie
import Link from 'next/link';
<Link href="/admissions" className="focus-visible:ring-2">
Toelatingsinformatie bekijken
</Link>
Hier is een volledig toegankelijke navigatiecomponent voor een universiteitsite:
// components/MainNav.tsx
import Link from 'next/link';
export function MainNav() {
return (
<header role="banner">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2 focus:text-lg"
>
Naar hoofdinhoud
</a>
<nav aria-label="Hoofdnavigatie">
<ul role="list">
<li><Link href="/admissions">Toelating</Link></li>
<li><Link href="/academics">Onderwijs</Link></li>
<li><Link href="/campus-life">Campusleven</Link></li>
<li><Link href="/research">Onderzoek</Link></li>
<li><Link href="/about">Over ons</Link></li>
</ul>
</nav>
</header>
);
}
Next.js heeft hier een natuurlijk voordeel. Omdat je React/JSX schrijft, stel je semantische elementen direct samen. Er is geen drag-and-drop paginabouwer die achter de schermen geneste <div>-soep genereert.
2. ARIA-labels en live-regio's
ARIA (Accessible Rich Internet Applications) attributen vullen de gaten in waar native HTML-semantiek niet volstaat. Maar hier is de gouden regel: geen ARIA is beter dan slechte ARIA. Gebruik eerst native HTML-elementen. Grijp naar ARIA alleen als je het echt nodig hebt.
Wanneer ARIA te gebruiken
aria-label: Voor knoppen die alleen uit pictogrammen bestaan zonder zichtbare tekst. Een vergrootglaszoekknop heeftaria-label="Zoeken"nodig.aria-describedby: Verbindt een invoer met het foutbericht zodat schermlezers beide lezen.aria-live="polite": Kondigt dynamische inhoudupdates aan (zoekresultaten laden, filterwijzigingen) zonder focus te stelen.role="alert": Voor urgente foutberichten die onmiddellijke aankondiging nodig hebben.aria-expanded: Geeft aan of een accordeon, vervolgkeuzelijst of menu geopend of gesloten is.
// components/SearchBar.tsx
import { useState, useRef } from 'react';
export function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isSearching, setIsSearching] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
setIsSearching(true);
// Haal resultaten op van uw API
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data.results);
setIsSearching(false);
}
return (
<div role="search" aria-label="Website zoeken">
<form onSubmit={handleSearch}>
<label htmlFor="site-search" className="sr-only">
De universiteitwebsite doorzoeken
</label>
<input
ref={inputRef}
id="site-search"
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Zoeken naar programma's, docenten, nieuws..."
autoComplete="off"
className="focus-visible:ring-2 focus-visible:ring-blue-600"
/>
<button type="submit" aria-label="Zoekopdracht indienen">
<SearchIcon aria-hidden="true" />
</button>
</form>
{/* Live-regio meldt resultaten aan schermlezers */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{isSearching
? 'Zoeken...'
: `${results.length} resultaten gevonden voor ${query}`
}
</div>
{results.length > 0 && (
<ul role="list" aria-label="Zoekresultaten">
{results.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
)}
</div>
);
}
Let op de aria-live="polite"-regio. Wanneer zoekresultaten laden, hoort een gebruiker van een schermlezer "5 resultaten gevonden voor informatica" zonder dat ze naar de resultatelijst hoeven te navigeren. Dat is het verschil tussen een toegankelijke site en een onbruikbare.

3. Toetsenbordnavigatie
Als een gebruiker met zicht er met muis op kan klikken, moet een toetsenbordgebruiker er met Tab op kunnen bereiken en het activeren met Enter of Spatie. Geen uitzonderingen.
De niet-onderhandelbare zaken
- Elk interactief element bereikbaar via Tab. Dit gebeurt automatisch als je semantische
<button>- en<a>-elementen gebruikt. - Logische tabvolgorde. De tabvolgorde moet de visuele inhoudsflow volgen. Gebruik niet
tabindex-waarden groter dan 0 -- het creëert chaos. - Zichtbare focusindicatoren. Schrijf nooit
outline: noneofoutline: 0op:focus. Ooit. Gebruik in plaats daarvan Tailwind'sfocus-visible:ring-2-- het toont de ring voor toetsenbordgebruikers maar niet voor muisklikken. - Overslaan-koppelingen. Het eerste focusbare element op de pagina moet een "Naar hoofdinhoud" koppeling zijn. Verborgen totdat deze geplaatst wordt.
- Focusvallen in modalen. Wanneer een modaal opent, moet Tab cyclisch werken in de modaal. Escape sluit het. Focus keert terug naar de triggerknop wanneer het sluit.
// components/AccessibleModal.tsx
import { useEffect, useRef, useCallback } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
triggerRef: React.RefObject<HTMLButtonElement>;
}
export function AccessibleModal({ isOpen, onClose, title, children, triggerRef }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const trapFocus = useCallback((e: KeyboardEvent) => {
if (!modalRef.current) return;
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstEl = focusableElements[0] as HTMLElement;
const lastEl = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstEl) {
e.preventDefault();
lastEl.focus();
} else if (!e.shiftKey && document.activeElement === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
}, [onClose]);
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus();
document.addEventListener('keydown', trapFocus);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', trapFocus);
document.body.style.overflow = '';
// Zet focus terug op trigger
if (!isOpen) triggerRef.current?.focus();
};
}, [isOpen, trapFocus, triggerRef]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
>
<div className="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
<div className="flex justify-between items-center mb-4">
<h2 id="modal-title" className="text-xl font-bold">{title}</h2>
<button
ref={closeButtonRef}
onClick={onClose}
aria-label="Dialoogvenster sluiten"
className="focus-visible:ring-2 focus-visible:ring-blue-600 rounded p-1"
>
✕
</button>
</div>
{children}
</div>
</div>
);
}
Dit patroon verzorgt focusvallen, Escape om te sluiten en focusteug naar vorige toestand. Ik heb variaties hiervan op minstens een dozijn onderwijssites uitgeleverd.
4. Kleurcontrast
Contrastverhouding zijn een van de meest mislukte WCAG-criteria -- en een van de gemakkelijkste om op te lossen als je je ontwerptoken vanaf het begin correct instelt.
| Teksttype | Minimale contrastverhouding (AA) | Voorbeeld |
|---|---|---|
| Normale tekst (< 18px) | 4,5:1 | Body copy, onderschriften, formlabels |
| Grote tekst (18px+ regulier, 14px+ vet) | 3:1 | Koppen, grote knoppen |
| Interactieve elementen | 3:1 tegen aangrenzende kleuren | Knopkaders, koppelingonderstreepingen |
| Niet-tekstelementen (pictogrammen, focusringen) | 3:1 | Formulierelveldkaders, grafiekelementen |
Regels verder dan verhouding
Gebruik nooit alleen kleur om informatie over te brengen. Een foutentoestand kan niet zomaar zijn "het veld wordt rood." Het heeft een rode rand nodig + een foutpictogram + een tekstlabel. Een gegevensvisualisatie kan niet uitsluitend op kleur vertrouwen om categorieën te onderscheiden -- gebruik patronen, labels of verschillende vormen.
Testtools
- Chrome DevTools Accessibility Panel: Controleer elk element, zie de contrastverhouding onmiddellijk.
- WebAIM Contrast Checker: Vul hexwaarden in, krijg slaag/zak voor AA en AAA.
- Figma-plugins (Stark, A11y): Vang contrastproblemen op voordat ze in code verschijnen.
Als referentiepunt: een gouden accent (#c8a96e) op een bijna zwarte achtergrond (#0a0a0b) geeft een contrastverhouding van 4,7:1 -- dat slaagt voor AA voor normale tekst. Ontwerptoken zijn belangrijk.
5. Alt-tekst voor afbeeldingen
Elk <img>-element heeft een alt-attribuut nodig. Wat erin gaat, hangt af van het doel van de afbeelding.
De beslissingsboom
- Informatieve afbeeldingen (foto's, illustraties die inhoud overbrengen): Schrijf beschrijvende alt-tekst. "Studenten studerend in de campusbibliotheek" niet "afbeelding123.jpg."
- Decoratieve afbeeldingen (achtergrondtexturen, visuele scheidingstekens, zuiver esthetisch): Gebruik
alt=""(lege string). Dit vertelt schermlezers om het over te slaan. - Grafieken en grafen: Schrijf gedetailleerde alt-tekst die de gegevens samenvat, of gebruik
aria-describedbydie wijst naar een gegevenstabel onder de grafiek. - Faculteitsfoto's:
alt="Dr. Sarah Chen, Associate Professor, Department of Computer Science" - **Programmaafbeeldingen:
alt="Engineeringsstudenten samenwerken aan een roboticaproject in het Maker Lab"
// ✅ Informatieve afbeelding
import Image from 'next/image';
<Image
src="/campus/library-study-area.jpg"
alt="Studenten studerend aan tafels in het atrium op drie verdiepingen van de bibliotheek van de Oprichters"
width={1200}
height={600}
/>
// ✅ Decoratieve afbeelding
<Image
src="/patterns/wave-divider.svg"
alt=""
role="presentation"
width={1200}
height={40}
/>
// ✅ Faculteitsfoto
<Image
src="/faculty/sarah-chen.jpg"
alt="Dr. Sarah Chen, Associate Professor, Afdeling Informatica"
width={300}
height={400}
/>
De Next.js Image-component waarschuwt je eigenlijk als je het alt-prop vergeet. Kleine dingen als dat tellen bij elkaar op.
6. Toegankelijke formulieren
Formulieren zijn waar onderwijswebsites op toegankelijkheid slagen of falen. Toepassingsformulieren, contactformulieren, cursusregistratie, financiële hulp -- als deze niet toegankelijk zijn, sluit je studenten uit die je diensten het meest nodig hebben.
// components/ContactForm.tsx
import { useState } from 'react';
export function ContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitted, setSubmitted] = useState(false);
function validate(formData: FormData) {
const newErrors: Record<string, string> = {};
if (!formData.get('name')) newErrors.name = 'Volledige naam is verplicht.';
if (!formData.get('email')) newErrors.email = 'E-mailadres is verplicht.';
const email = formData.get('email') as string;
if (email && !email.includes('@')) newErrors.email = 'Voer een geldig e-mailadres in.';
if (!formData.get('message')) newErrors.message = 'Bericht is verplicht.';
return newErrors;
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const newErrors = validate(formData);
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) setSubmitted(true);
}
if (submitted) {
return <div role="alert"><p>Bedankt! We nemen binnen 2 werkdagen contact op.</p></div>;
}
return (
<form onSubmit={handleSubmit} noValidate>
{/* Foutensamenvatting */}
{Object.keys(errors).length > 0 && (
<div role="alert" className="bg-red-50 border-red-500 border p-4 mb-6 rounded">
<h2 className="text-red-800 font-bold mb-2">
Corrigeer alstublieft {Object.keys(errors).length} fout(en):
</h2>
<ul>
{Object.entries(errors).map(([field, msg]) => (
<li key={field}>
<a href={`#field-${field}`} className="text-red-700 underline">{msg}</a>
</li>
))}
</ul>
</div>
)}
<div className="mb-4">
<label htmlFor="field-name" className="block font-medium mb-1">
Volledige naam <span aria-hidden="true">*</span>
</label>
<input
id="field-name"
name="name"
type="text"
autoComplete="name"
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'error-name' : undefined}
className="w-full border rounded px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-600"
/>
{errors.name && (
<p id="error-name" className="text-red-600 text-sm mt-1" role="alert">
{errors.name}
</p>
)}
</div>
<div className="mb-4">
<label htmlFor="field-email" className="block font-medium mb-1">
E-mailadres <span aria-hidden="true">*</span>
</label>
<input
id="field-email"
name="email"
type="email"
autoComplete="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'error-email' : undefined}
className="w-full border rounded px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-600"
/>
{errors.email && (
<p id="error-email" className="text-red-600 text-sm mt-1" role="alert">
{errors.email}
</p>
)}
</div>
<div className="mb-4">
<label htmlFor="field-message" className="block font-medium mb-1">
Bericht <span aria-hidden="true">*</span>
</label>
<textarea
id="field-message"
name="message"
rows={5}
aria-required="true"
aria-invalid={!!errors.message}
aria-describedby={errors.message ? 'error-message' : undefined}
className="w-full border rounded px-3 py-2 focus-visible:ring-2 focus-visible:ring-blue-600"
/>
{errors.message && (
<p id="error-message" className="text-red-600 text-sm mt-1" role="alert">
{errors.message}
</p>
)}
</div>
<button
type="submit"
className="bg-blue-700 text-white px-6 py-3 rounded font-medium hover:bg-blue-800 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-600"
>
Bericht verzenden
</button>
</form>
);
}
Let op de foutensamenvatting bovenaan met ankerlinks naar elk problematisch veld. De aria-describedby-verbindingen. De autoComplete-attributen. De aria-required- en aria-invalid-toestanden. Dit is wat een toegankelijk formulier er werkelijk uitziet.
7. Video en multimedia
Universiteitwebsites zitten vol video -- virtuele campusrondleidingen, collegeopnamen, toespraken van de president, getuigenissen van studenten. Elk hiervan heeft accessible alternatieven nodig.
De vereisten
- Ondertitels voor alle videoinhoud. Auto-gegenereerde ondertitels (YouTube, Rev.ai) zijn een startpunt, maar moeten door menselijk gekeurd worden. Auto-ondertitels hebben 10-15% foutpercentages -- onaanvaardbaar voor academische inhoud.
- Audiobeschrijvingen voor visueel-alleen inhoud. Uw virtuele campusrondleidingsvideoтing met prachtige gebouwen? Een blinde gebruiker hoort stilte tenzij je beschrijft wat op het scherm is.
- Transcripten beschikbaar voor alle multimedia. Een downloadbare of op-pagina tekstversie.
- Pauze-/stopbesturingselementen voor elke auto-afgespeelde inhoud.
- Geen audio auto-afspelen die gebruikers niet onmiddellijk kunnen stoppen.
// Toegankelijk video-insluitpatroon
<figure>
<div className="relative aspect-video">
<iframe
src="https://www.youtube.com/embed/VIDEO_ID?cc_load_policy=1"
title="Virtuele rondleiding door het Engineering Building, inclusief labs, klaslokalen en studentenruimtes"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="absolute inset-0 w-full h-full"
/>
</div>
<figcaption className="mt-2 text-sm text-gray-600">
Virtuele rondleiding door het Engineering Building.
<a href="/transcripts/engineering-tour" className="underline ml-1">
Lees het volledige transcript
</a>
</figcaption>
</figure>
Het title-attribuut op de iframe is kritiek -- schermlezers kondigen het aan wanneer de gebruiker de insluit bereikt. De cc_load_policy=1-parameter dwingt ondertitels standaard in op YouTube-insluitingen.
8. Geautomatiseerde testen in CI/CD
Handmatig toegankelijkheidstesten is noodzakelijk maar onvoldoende. Je hebt geautomatiseerde controles nodig die regressies verhinderen vanuit productie te bereiken.
De pijpelin
- Lighthouse CI in je GitHub Actions of Vercel-build: Stel een drempel in en faal de build als de toegankelijkheidsscore onder 90 zakt.
- axe-core integratie: Voer geautomatiseerde WCAG 2.1 AA-scans uit op elk component tijdens unit/integratietesten.
- Handmatig toetsenbordtesten: Navigeer voordat je een grote release indient, de gehele site met alleen Tab, Enter, Spatie en pijltjestoetsen.
- Schermlezer testen: Elk kwartaal testen met VoiceOver (Mac), NVDA (Windows) en TalkBack (Android).
# .github/workflows/accessibility.yml
name: Accessibility Audit
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000/
http://localhost:3000/admissions
http://localhost:3000/academics
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
// lighthouse-budget.json
[{
"path": "/*",
"options": {
"assertions": {
"categories:accessibility": ["error", { "minScore": 0.9 }]
}
}
}]
Voor componentniveautesten met axe-core:
// __tests__/ContactForm.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { ContactForm } from '../components/ContactForm';
expect.extend(toHaveNoViolations);
test('ContactForm heeft geen toegankelijkheidsovertredingen', async () => {
const { container } = render(<ContactForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Implementeer nooit een ontoegankelijke pagina. Bouw de vangrails in je pijpelin in, en je hoeft dit niet te doen.
Waarom WordPress en Drupal worstelen met toegankelijkheid
Ik zeg niet dat WordPress kan toegankelijk zijn. Ik zeg dat in de praktijk bijna nooit -- zeker niet voor onderwijssites met complexe vereisten.
Dit is waarom:
- Toegankelijkheid hangt af van elke geïnstalleerde plugin die toegankelijk is. Je contactformulier-plugin, je evenementenkalender-plugin, je mega-menu-plugin, je schuifpuzzel-plugin -- elk ervan moet geldig, toegankelijk markup produceren. De meeste doen dat niet.
- Plugin-updates breken dingen. Een WooCommerce-update of Elementor-update kan stilzwijgend toegankelijkheidregressions introduceren. Je weet het niet totdat iemand klaagt -- of aanklacht.
- Geen geautomatiseerde toegankelijkheidkontroleer in de deploy-pijpelin. Standaard WordPress-implementaties bevatten geen Lighthouse-gates of axe-core-scans. Wijzigingen worden live gezet zonder enige toegankelijkheidverificatie.
- Content-auteurs creëren ontoegankelijke inhoud. WYSIWYG-editors laten gebruikers koppelingniveaus overslaan, afbeeldingen zonder alt-tekst invoegen en koppelingen maken die zeggen "klik hier." Er is geen afdwingingsmechanisme.
Ik heb WordPress-onderwijssites geaudit die Lighthouse 42 scoorden na een thema-update. 42. De school wist het niet totdat we het hun vertelden.
Het headless-voordeel: toegankelijkheid afgedwongen bij build-tijd
De benadering die we nemen met Next.js development en headless CMS-architectuur keert het model om. Toegankelijkheid wordt niet achteraf gerepareerd -- het wordt afgedwongen bij build-tijd.
| Benadering | Toegankelijkheidshandhaving | Typische Lighthouse-score | Regressierisco |
|---|---|---|---|
| WordPress + plugins | Handmatige audits, overlaytools | 40-65 | Hoog (elke plugin-update) |
| Drupal + contrib-modules | Beter dan WP, nog steeds handmatig | 55-75 | Gemiddeld |
| Next.js + headless CMS | CI/CD-automatie, build-tijd | 90-100 | Laag (geautomatiseerde gates) |
Semantische HTML is de React-standaard. Tailwind's focus-visible-utilities zijn een enkele klasse. De CI/CD Lighthouse-controle voorkomt regressions. Eén codebase betekent consistente compliance over elk school-, afdeling- en programmapagina -- niet 47 verschillende WordPress-installaties met 47 verschillende plugin-configuraties.
Als je een wederopbouw of migratie overweegt, helpen we graag de specifieke zaken door te nemen. Bekijk onze mogelijkheden of neem contact op. En als je nieuwsgierig bent wat een headless onderwijssite-project er financieel uitziet, heeft onze prijspagina transparante getallen.
Veelgestelde vragen
Geldt WCAG 2.1 AA-compliance voor alle schoolwebsites, inclusief K-12? Ja. Openbare schooldistricten ontvangen federale financiering, wat Sectie 508-vereisten triggert. De DOJ's 2024 definitieve regel onder ADA Titel II dekt staatsorganen en lokale overheidsinstellingen, wat openbare schooldistricten omvat. Particuliere K-12-scholen kunnen ook onder ADA Titel III vallen. De veilige aanname: als je een schoolwebsite runt, geldt WCAG 2.1 AA voor jou.
Wat is het verschil tussen WCAG 2.1 AA en WCAG 2.2 AA? WCAG 2.2, gepubliceerd in oktober 2023, voegt negen nieuwe succescriteria toe bovenop 2.1. De DOJ's 2024-regel verwijst specifiek naar WCAG 2.1 AA als de compliancenorm voorlopig. Het is echter slim om WCAG 2.2 AA na te streven als toekomstplanning. De nieuwe criteria richten zich op dingen zoals focusverschijning, sleepbewegingen en consistente hulp -- allemaal relevant voor onderwijssites met complexe formulieren en navigatie.
Kan een toegankelijkheid-overlaytools zoals accessiBe of UserWay onze site compatibel maken? Nee. De National Federation of the Blind en meerdere gerechtshoven hebben verklaard dat overlaytools geen WCAG-compliance bieden. Eigenlijk hebben sommige eisers specifiek de aanwezigheid van overlaytools aangehaald als bewijs dat de tegenpartij wist dat hun site ontoegankelijk was maar een cosmetische oplossing koos in plaats van een echte. Repareer de broncode.
Hoeveel kost het om een bestaande onderwijswebsite voor WCAG 2.1 AA te herstelllen? Herstelkosten variëren enorm afhankelijk van de huidige toestand van de site. Voor een typische WordPress-universiteitsite, verwacht $50K-$150K+ voor een grondige hersteking. Veel instellingen vinden het kosteneffectiever om op een modern, standaard toegankelijk stapel als Next.js opnieuw op te bouwen, waarbij de totale projectkosten ($75K-$200K+) volledige WCAG-compliance van dag één omvatten plus dramatisch lagere lopende onderhoudskosten.
Welke Lighthouse-toegankelijkheidsscore moeten we nastre? Minimaal 90, doelstelling 95+. Maar begrijp dat Lighthouse slechts ongeveer 30-40% van WCAG 2.1 AA-kwesties can vatten. Het kan contrastverhouding verifiëren, alt-tekstaanwezigheid en ARIA-kenmerk-geldigheid, maar het kan niet testen of je tabvolgorde logisch is, of je overslaan-koppelingen werken of je inhoud voor een schermlezergebruiker logisch is. Geautomatiseerde testen plus handmatige testen is het enige echte antwoord.
Hoe vaak moeten we onze onderwijswebsite op toegankelijkheid testen? Geautomatiseerde testen moeten op elke pull-aanvraag draaien -- dat is wat de CI/CD-pijpelin voor is. Handmatig toetsenbordnavigatie-testen moet vóór elke grote release plaatsvinden. Schermlezer-testen (VoiceOver, NVDA) moeten minstens elk kwartaal plaatsvinden. Een volledige professionele WCAG-audit moet jaarlijks plaatsvinden of wanneer belangrijke functies worden toegevoegd.
Maakt Next.js een website automatisch toegankelijk?
Geen framework is automatisch toegankelijk -- je moet nog steeds goed code schrijven. Maar Next.js biedt belangrijke voordelen: de Image-component waarschuwt voor ontbrekende alt-tekst, de Link-component genereert correcte <a>-tags met juiste href-afhandeling, React's JSX moedigt semantische elementen aan, en de build-pijpelin ondersteunt geautomatiseerde toegankelijkheidstesten. Het framework doet het werk niet voor je, maar het maakt het juiste ding doen het pad met de minste weerstand.
Wat zijn de straffen voor een ontoegankelijke universiteitwebsite? ADA-schikkingen voor onderwijswebsites variëren van $25.000 tot meer dan $300.000, plus advocatenhonorariums, plus herstelkosten (die de schikking zelf kunnen overschrijden). Naast geldelijke straffen kan OCR nalevingovereenkoomsten vereisen die voortdurende monitoring en rapportage voor jaren voorschrijven. En daar is de reputatieschade -- de soort die potentiële studenten en faculteiten doen nadenken over uw instelling.