Conformité WCAG 2.1 AA pour les sites éducatifs : Liste de contrôle Next.js
Conformité WCAG 2.1 AA pour les sites web éducatifs : Liste de vérification Next.js
En 2024 seul, plus de 200 poursuites liées à l'ADA ont visé les établissements d'enseignement pour des sites web inaccessibles. Les règlements variaient de 25 000 $ à 300 000 $ +, et c'est avant les frais de correction. Votre site web universitaire ou de district scolaire est une cible juridique s'il ne respecte pas les normes WCAG 2.1 AA.
Voici ce que la plupart des agences ne vous diront pas : la même technologie qui rend votre site web plus rapide le rend également plus accessible. Un site Next.js créé avec du HTML sémantique et Tailwind CSS obtient des scores d'accessibilité Lighthouse de 95+ sur 100 prêts à l'emploi. Un site WordPress typique avec 30 plugins ? Il se situe quelque part entre 40 et 60. J'ai audité suffisamment de sites web éducatifs pour savoir que le choix de plateforme que vous faites aujourd'hui détermine si vous déposez des tickets de correction ou si vous dormez paisiblement l'année prochaine.
Ce n'est pas un aperçu théorique. C'est une liste de vérification technique de travail -- huit catégories, des exemples de code réels, et les motifs d'implémentation exacts que nous utilisons lors de la création de sites éducatifs accessibles chez Social Animal. Marquez-le. Partagez-le avec votre équipe de développement. Imprimez-le et collez-le au mur.
Table des matières
- Le contexte juridique : Pourquoi les sites web éducatifs sont des cibles
- 1. HTML sémantique
- 2. Étiquettes ARIA et régions dynamiques
- 3. Navigation au clavier
- 4. Contraste des couleurs
- 5. Texte alternatif pour les images
- 6. Formulaires accessibles
- 7. Vidéo et multimédia
- 8. Test automatisé dans CI/CD
- Pourquoi WordPress et Drupal ont du mal avec l'accessibilité
- L'avantage headless : Accessibilité appliquée au moment de la compilation
- FAQ

Le contexte juridique : Pourquoi les sites web éducatifs sont des cibles
Commençons par les questions juridiques car c'est ce qui attire l'attention de votre directeur financier.
La section 508 de la Loi sur la réadaptation s'applique à tous les organismes fédéraux et à tout établissement qui reçoit un financement fédéral. C'est chaque université publique. Chaque district scolaire public. Si votre établissement reçoit un seul dollar de financement fédéral -- y compris les subventions Pell, le financement de la recherche, ou les fonds Title I -- la section 508 s'applique à vous.
Le titre III de l'ADA couvre les lieux d'accommodations publiques. Les tribunaux ont régulièrement jugé que cela inclut les universités privées et leurs sites web. Harvard, MIT, et d'innombrables institutions plus petites ont été poursuivies en vertu du titre III.
WCAG 2.1 AA est la norme technique que les tribunaux consultent lors de l'évaluation de la conformité. Le DOJ a émis une règle finale en 2024 déclarant explicitement que les sites web des gouvernements d'État et locaux (y compris les universités publiques et les districts scolaires) doivent être conformes à WCAG 2.1 Niveau AA. Ce n'est pas une suggestion. C'est une règle avec des échéances d'application.
Les chiffres racontent l'histoire : les poursuites en vertu de l'ADA contre les établissements d'enseignement ont augmenté d'environ 300 % entre 2018 et 2025. Le Bureau pour les droits civiques (OCR) a résolu plus de 15 000 plaintes au cours de l'exercice 2024, l'accessibilité numérique étant l'une des catégories de plaintes à la croissance la plus rapide.
| Cadre juridique | S'applique à | Norme | Échéance clé |
|---|---|---|---|
| Section 508 | Universités publiques, districts scolaires (bénéficiaires de financement fédéral) | WCAG 2.1 AA | Déjà applicable |
| Titre II ADA (Règle DOJ 2024) | Entités gouvernementales d'État/locales | WCAG 2.1 AA | Avril 2026 (grandes entités), Avril 2027 (petites) |
| Titre III ADA | Universités privées, écoles K-12 privées | WCAG 2.1 AA (de facto) | Les tribunaux appliquent maintenant |
| Lois d'État (CA, NY, etc.) | Varie selon l'État | WCAG 2.1 AA typique | Varie |
Construisons maintenant un site web qui passe réellement.
1. HTML sémantique
Le HTML sémantique est la base de tout. Si vous vous trompez là-dessus, aucune quantité d'attributs ARIA ne vous sauvera. Les lecteurs d'écran s'appuient sur la structure sémantique du document pour aider les utilisateurs à comprendre la hiérarchie de la page et à naviguer entre les sections.
Hiérarchie des en-têtes
Chaque page obtient exactement un <h1>. Les sous-en-têtes suivent dans l'ordre : <h2>, puis <h3>, puis <h4>. Ne sautez jamais de niveaux. Un utilisateur de lecteur d'écran qui entend « En-tête niveau 4 » après « En-tête niveau 2 » perd le contexte.
Je vois les sites web éducatifs violer cette règle constamment -- particulièrement les pages de département où quelqu'un dans un CMS a collé du contenu avec des niveaux d'en-tête aléatoires parce que la taille de police semblait correcte visuellement.
Éléments de repère
Utilisez <nav>, <main>, <aside>, <footer>, et <header>. Ceux-ci créent des régions navigables pour les utilisateurs de lecteur d'écran. Un utilisateur JAWS ou NVDA peut appuyer sur une seule touche pour sauter entre les repères.
Boutons vs. Liens
Cela me monte au plafond. Utilisez <button> pour les éléments interactifs qui effectuent une action (ouvrir un menu, soumettre un formulaire, basculer un filtre). Utilisez <a> pour la navigation qui amène l'utilisateur à une nouvelle page ou section. Ne jamais utiliser un <div> avec un gestionnaire onClick.
// ❌ Faux : div prétendant être un bouton
<div onClick={handleClick} className="cursor-pointer">
Ouvrir le menu
</div>
// ❌ Faux : bouton utilisé pour la navigation
<button onClick={() => router.push('/admissions')}>
Afficher les admissions
</button>
// ✅ Correct : bouton sémantique pour les actions
<button
onClick={handleMenuToggle}
aria-expanded={isOpen}
aria-controls="main-nav"
className="focus-visible:ring-2 focus-visible:ring-offset-2"
>
Ouvrir le menu
</button>
// ✅ Correct : ancre pour la navigation
import Link from 'next/link';
<Link href="/admissions" className="focus-visible:ring-2">
Afficher les admissions
</Link>
Voici un composant de navigation complet accessible pour un site universitaire :
// 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"
>
Passer au contenu principal
</a>
<nav aria-label="Navigation principale">
<ul role="list">
<li><Link href="/admissions">Admissions</Link></li>
<li><Link href="/academics">Études</Link></li>
<li><Link href="/campus-life">Vie du campus</Link></li>
<li><Link href="/research">Recherche</Link></li>
<li><Link href="/about">À propos</Link></li>
</ul>
</nav>
</header>
);
}
Next.js a un avantage naturel ici. Parce que vous écrivez React/JSX, vous composez directement des éléments sémantiques. Il n'y a pas de constructeur de pages glisser-déposer générant du HTML <div> imbriqué en arrière-plan.
2. Étiquettes ARIA et régions dynamiques
ARIA (Accessible Rich Internet Applications) remplit les lacunes là où la sémantique HTML native n'est pas suffisante. Mais voici la règle d'or : pas d'ARIA est mieux que du mauvais ARIA. Utilisez d'abord les éléments HTML natifs. N'utilisez ARIA que si vous en avez besoin.
Quand utiliser ARIA
aria-label: Pour les boutons contenant seulement des icônes où il n'y a pas de texte visible. Un bouton de recherche avec une loupe a besoin dearia-label="Rechercher".aria-describedby: Lie une saisie à son message d'erreur pour que les lecteurs d'écran lisent les deux.aria-live="polite": Annonce les mises à jour de contenu dynamique (résultats de recherche en cours de chargement, changements de filtre) sans voler le focus.role="alert": Pour les messages d'erreur urgents qui nécessitent une annonce immédiate.aria-expanded: Communique si un accordéon, un menu déroulant, ou un menu est ouvert ou fermé.
// 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);
// Récupérer les résultats depuis votre API
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
setResults(data.results);
setIsSearching(false);
}
return (
<div role="search" aria-label="Recherche du site">
<form onSubmit={handleSearch}>
<label htmlFor="site-search" className="sr-only">
Rechercher sur le site web de l'université
</label>
<input
ref={inputRef}
id="site-search"
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Rechercher des programmes, des professeurs, des actualités..."
autoComplete="off"
className="focus-visible:ring-2 focus-visible:ring-blue-600"
/>
<button type="submit" aria-label="Soumettre la recherche">
<SearchIcon aria-hidden="true" />
</button>
</form>
{/* La région dynamique annonce les résultats aux lecteurs d'écran */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{isSearching
? 'Recherche en cours...'
: `${results.length} résultats trouvés pour ${query}`
}
</div>
{results.length > 0 && (
<ul role="list" aria-label="Résultats de recherche">
{results.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
)}
</div>
);
}
Remarquez la région aria-live="polite". Lorsque les résultats de recherche se chargent, un utilisateur de lecteur d'écran entend « 5 résultats trouvés pour informatique » sans avoir à naviguer vers la liste des résultats. C'est la différence entre un site accessible et un site inutilisable.

3. Navigation au clavier
Si un utilisateur voyant peut cliquer dessus, un utilisateur au clavier doit être capable de l'atteindre avec Tab et l'activer avec Entrée ou Espace. Aucune exception.
Les indispensables
- Chaque élément interactif accessible via Tab. Cela se produit automatiquement si vous utilisez des éléments sémantiques
<button>et<a>. - Ordre de tabulation logique. L'ordre de tabulation doit suivre le flux du contenu visuel. N'utilisez pas de valeurs
tabindexsupérieures à 0 -- c'est du chaos. - Indicateurs de focus visibles. Ne jamais écrire
outline: noneououtline: 0sur:focus. Jamais. Utilisez plutôtfocus-visible:ring-2de Tailwind -- c'est affiche l'anneau pour les utilisateurs au clavier mais pas pour les clics à la souris. - Liens de saut. Le tout premier élément pouvant recevoir le focus sur la page doit être un lien « Passer au contenu principal ». Caché jusqu'au focus.
- Piégeage du focus dans les modales. Lorsqu'une modale s'ouvre, Tab doit faire défiler dans la modale. Échap la ferme. Le focus revient au bouton déclencheur lorsqu'il se ferme.
// 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 = '';
// Retourner le focus au déclencheur
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="Fermer la boîte de dialogue"
className="focus-visible:ring-2 focus-visible:ring-blue-600 rounded p-1"
>
✕
</button>
</div>
{children}
</div>
</div>
);
}
Ce motif gère le piégeage du focus, Échap pour fermer, et la restauration du focus. J'ai livré des variations de ceci sur au moins une douzaine de sites web éducatifs.
4. Contraste des couleurs
Les rapports de contraste sont l'un des critères WCAG les plus fréquemment échoués -- et l'un des plus faciles à corriger si vous configurez correctement vos jetons de conception dès le départ.
| Type de texte | Rapport de contraste minimum (AA) | Exemple |
|---|---|---|
| Texte normal (< 18px) | 4.5:1 | Copie du corps, légendes, étiquettes de formulaire |
| Texte grand (18px+ normal, 14px+ gras) | 3:1 | En-têtes, grands boutons |
| Éléments interactifs | 3:1 contre les couleurs adjacentes | Bordures de champ de formulaire, soulignements de lien |
| Éléments non-texte (icônes, anneaux de focus) | 3:1 | Bordures de champ de formulaire, éléments de graphique |
Règles au-delà des rapports
Ne jamais utiliser la couleur seule pour communiquer des informations. Un état d'erreur ne peut pas simplement être « le champ devient rouge ». Il doit avoir une bordure rouge + une icône d'erreur + une étiquette de texte. Une visualisation de données ne peut pas dépendre uniquement de la couleur pour distinguer les catégories -- utilisez des motifs, des étiquettes, ou des formes distinctes.
Outils de test
- Panneau d'accessibilité de Chrome DevTools : Inspectez n'importe quel élément, voyez son rapport de contraste instantanément.
- Vérificateur de contraste WebAIM : Branchez les valeurs hex, obtenez un résultat réussi/échoué pour AA et AAA.
- Plugins Figma (Stark, A11y) : Attrapez les problèmes de contraste avant qu'ils ne touchent le code.
À titre de référence : une accent or (#c8a96e) sur un fond noir quasi ( #0a0a0b) produit un rapport de contraste de 4,7:1 -- c'est conforme à AA pour le texte normal. Les jetons de conception sont importants.
5. Texte alternatif pour les images
Chaque élément <img> a besoin d'un attribut alt. Ce qu'il contient dépend du but de l'image.
L'arbre décisionnel
- Images informatives (photos, illustrations qui communiquent du contenu) : Écrivez du texte alt descriptif. « Des étudiants étudiant dans la bibliothèque du campus » plutôt que « image123.jpg ».
- Images décoratives (textures de fond, diviseurs visuels, purement esthétiques) : Utilisez
alt=""(chaîne vide). Cela indique aux lecteurs d'écran de la sauter. - Graphiques et graphiques : Soit écrivez un texte alt détaillé résumant les données, soit utilisez
aria-describedbypointant vers un tableau de données sous le graphique. - Photos de professeurs :
alt="Dr. Sarah Chen, Professeure associée, Département d'informatique" - Images principales de programme : Décrivez le contexte de la scène.
alt="Les étudiants en ingénierie collaborant sur un projet de robotique au Maker Lab"
// ✅ Image informative
import Image from 'next/image';
<Image
src="/campus/library-study-area.jpg"
alt="Des étudiants étudiant à des tables dans l'atrium à trois étages de la bibliothèque Founders"
width={1200}
height={600}
/>
// ✅ Image décoratives
<Image
src="/patterns/wave-divider.svg"
alt=""
role="presentation"
width={1200}
height={40}
/>
// ✅ Photo de professeur
<Image
src="/faculty/sarah-chen.jpg"
alt="Dr. Sarah Chen, Professeure associée, Département d'informatique"
width={300}
height={400}
/>
Le composant Image de Next.js vous avertit en fait si vous oubliez la propriété alt. Les petites choses comme celle-ci s'accumulent.
6. Formulaires accessibles
Les formulaires sont là où les sites web éducatifs vivent ou meurent sur l'accessibilité. Formulaires de candidature, formulaires de contact, inscription aux cours, aide financière -- si ceux-ci ne sont pas accessibles, vous excluez les étudiants qui ont le plus besoin de vos services.
// 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 = 'Le nom complet est requis.';
if (!formData.get('email')) newErrors.email = 'L\'adresse e-mail est requise.';
const email = formData.get('email') as string;
if (email && !email.includes('@')) newErrors.email = 'Veuillez entrer une adresse e-mail valide.';
if (!formData.get('message')) newErrors.message = 'Le message est requis.';
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>Merci ! Nous vous contacterons dans les 2 jours ouvrables.</p></div>;
}
return (
<form onSubmit={handleSubmit} noValidate>
{/* Résumé des erreurs */}
{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">
Veuillez corriger {Object.keys(errors).length} erreur(s) :
</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">
Nom complet <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">
Adresse e-mail <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">
Message <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"
>
Envoyer le message
</button>
</form>
);
}
Remarquez le résumé des erreurs en haut avec des liens d'ancre vers chaque champ problématique. Les connexions aria-describedby. Les attributs autoComplete. Les états aria-required et aria-invalid. Voilà à quoi ressemble un formulaire réellement accessible.
7. Vidéo et multimédia
Les sites web universitaires sont remplis de vidéos -- visites virtuelles du campus, enregistrements de cours, discours du président, témoignages d'étudiants. Chacun d'eux doit disposer d'alternatives accessibles.
Les exigences
- Légendes pour tout contenu vidéo. Les légendes générées automatiquement (YouTube, Rev.ai) sont un point de départ, mais elles doivent être examinées par un humain. Les légendes générées automatiquement ont des taux d'erreur de 10-15 % -- inacceptable pour le contenu académique.
- Descriptions audio pour le contenu visuel uniquement. Votre vidéo de visite virtuelle du campus montrant de beaux bâtiments ? Un utilisateur aveugle entend le silence à moins que vous ne narriez ce qui est à l'écran.
- Transcriptions disponibles pour tous les contenus multimédias. Une version texte téléchargeable ou en page.
- Contrôles de pause/arrêt pour tout contenu en lecture automatique.
- Pas de lecture automatique audio que les utilisateurs ne peuvent pas arrêter immédiatement.
// Motif d'intégration vidéo accessible
<figure>
<div className="relative aspect-video">
<iframe
src="https://www.youtube.com/embed/VIDEO_ID?cc_load_policy=1"
title="Visite virtuelle du bâtiment d'ingénierie, y compris les laboratoires, les salles de classe et les espaces étudiants"
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">
Visite virtuelle du bâtiment d'ingénierie.
<a href="/transcripts/engineering-tour" className="underline ml-1">
Lire la transcription complète
</a>
</figcaption>
</figure>
L'attribut title sur l'iframe est critique -- les lecteurs d'écran l'annoncent lorsque l'utilisateur atteint l'intégration. Le paramètre cc_load_policy=1 force les légendes activées par défaut dans les intégrations YouTube.
8. Test automatisé dans CI/CD
Le test d'accessibilité manuel est nécessaire mais insuffisant. Vous avez besoin de vérifications automatisées qui empêchent les régressions de jamais atteindre la production.
Le pipeline
- Lighthouse CI dans vos GitHub Actions ou votre construction Vercel : Définissez un seuil et échouez la construction si le score d'accessibilité descend en dessous de 90.
- Intégration axe-core : Exécutez des analyses automatisées WCAG 2.1 AA sur chaque composant pendant les tests unitaires/intégration.
- Test de navigation au clavier manuel : Avant chaque version majeure, naviguez sur l'ensemble du site en utilisant uniquement Tab, Entrée, Espace, et les touches fléchées.
- Test du lecteur d'écran : Test trimestriel avec VoiceOver (Mac), NVDA (Windows), et TalkBack (Android).
# .github/workflows/accessibility.yml
name: Audit d'accessibilité
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: Exécuter 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 }]
}
}
}]
Pour le test au niveau des composants avec 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 n\'a pas de violations d\'accessibilité', async () => {
const { container } = render(<ContactForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Ne jamais déployer une page inaccessible. Construisez les garde-fous dans votre pipeline, et vous n'aurez pas besoin.
Pourquoi WordPress et Drupal ont du mal avec l'accessibilité
Je ne dis pas que WordPress ne peut pas être accessible. Je dis qu'en pratique, c'est presque jamais le cas -- particulièrement pour les sites web éducatifs aux exigences complexes.
Voici pourquoi :
- L'accessibilité dépend de chaque plugin installé étant accessible. Votre plugin de formulaire de contact, votre plugin de calendrier d'événements, votre plugin de mega menu, votre plugin de curseur -- chacun doit produire un HTML valide et accessible. La plupart ne le font pas.
- Les mises à jour de plugins cassent les choses. Une mise à jour WooCommerce ou Elementor peut introduire silencieusement des régressions d'accessibilité. Vous ne le saurez pas jusqu'à ce que quelqu'un se plaigne -- ou poursuive.
- Aucune vérification automatique d'accessibilité dans le pipeline de déploiement. Les déploiements WordPress standard n'incluent pas les portes Lighthouse ou les analyses axe-core. Les modifications vont en direct sans aucune vérification d'accessibilité.
- Les auteurs de contenu créent du contenu inaccessible. Les éditeurs WYSIWYG permettent aux utilisateurs de sauter les niveaux de titre, d'insérer des images sans texte alt, et de créer des liens qui disent « cliquez ici ». Il n'y a aucun mécanisme d'application.
J'ai audité des sites WordPress éducatifs qui ont obtenu 42 de Lighthouse après une mise à jour de thème. 42. L'école ne le savait pas jusqu'à ce que nous le leur disions.
L'avantage headless : Accessibilité appliquée au moment de la compilation
L'approche que nous prenons avec le développement Next.js et l'architecture de CMS headless renverse le modèle. L'accessibilité n'est pas un pansement appliqué après coup -- elle est appliquée au moment de la compilation.
| Approche | Application de l'accessibilité | Score Lighthouse typique | Risque de régression |
|---|---|---|---|
| WordPress + plugins | Audits manuels, outils superposés | 40-65 | Élevé (chaque mise à jour de plugin) |
| Drupal + modules contrib | Mieux que WP, encore manuel | 55-75 | Moyen |
| Next.js + CMS headless | Automatisation CI/CD, compilation | 90-100 | Faible (portes automatisées) |
Le HTML sémantique est la valeur par défaut React. Les utilitaires focus-visible de Tailwind sont une seule classe. La vérification Lighthouse CI/CD empêche les régressions. Une base de code signifie une conformité cohérente sur chaque page d'école, de département et de programme -- pas 47 installations WordPress différentes avec 47 configurations de plugin différentes.
Si vous envisagez une reconstruction ou une migration, nous serions heureux de discuter des spécificités. Consultez nos capacités ou contactez-nous. Et si vous êtes curieux de savoir à quoi ressemble un projet de site éducatif headless du point de vue du budget, notre page de tarification contient des chiffres transparents.
FAQ
La conformité WCAG 2.1 AA s'applique-t-elle à tous les sites web scolaires, y compris le primaire-secondaire ?
Oui. Les districts scolaires publics reçoivent un financement fédéral, ce qui déclenche les exigences de la section 508. La règle finale du DOJ en 2024 en vertu du titre II ADA couvre les entités gouvernementales d'État et locales, ce qui inclut les districts scolaires publics. Les écoles K-12 privées peuvent également être couvertes en vertu du titre III ADA. L'hypothèse sûre : si vous dirigez un site web scolaire, WCAG 2.1 AA s'applique à vous.
Quelle est la différence entre WCAG 2.1 AA et WCAG 2.2 AA ?
WCAG 2.2, publié en octobre 2023, ajoute neuf nouveaux critères de succès en plus de ceux de 2.1. La règle 2024 du DOJ fait spécifiquement référence à WCAG 2.1 AA comme norme de conformité pour le moment. Cependant, viser WCAG 2.2 AA est une bonne planification anticipée. Les nouveaux critères se concentrent sur des choses comme l'apparence du focus, les mouvements de traînage, et l'aide cohérente -- tout pertinent pour les sites web éducatifs avec des formulaires et des navigations complexes.
Un outil de superposition d'accessibilité comme accessiBe ou UserWay peut-il rendre notre site conforme ?
Non. La National Federation of the Blind et plusieurs décisions judiciaires ont déclaré que les outils de superposition ne fournissent pas la conformité WCAG. En fait, certains plaignants ont spécifiquement cité la présence d'outils de superposition comme preuve que le défendeur savait que son site était inaccessible mais a choisi un correctif cosmétique à la place d'un vrai. Réparez le code source.
Combien coûte la correction d'un site web éducatif existant pour WCAG 2.1 AA ?
Les coûts de correction varient énormément selon l'état actuel du site. Pour un site WordPress universitaire typique, prévoyez 50 000 $ à 150 000 $ + pour une correction complète. De nombreuses institutions trouvent plus rentable de reconstruire sur une pile moderne et accessible par défaut comme Next.js, où le coût total du projet (75 000 $ à 200 000 $) inclut la conformité WCAG complète dès le départ plus des coûts de maintenance nettement inférieurs.
Quel score d'accessibilité Lighthouse devons-nous viser ?
Minimum 90, cible 95+. Mais comprenez que Lighthouse ne capture que 30-40 % des problèmes WCAG 2.1 AA. Il peut vérifier les rapports de contraste, la présence de texte alt, et la validité des attributs ARIA, mais il ne peut pas tester si votre ordre de tabulation est logique, si vos liens de saut fonctionnent, ou si votre contenu a du sens pour un utilisateur de lecteur d'écran. Le test automatisé plus le test manuel est la seule vraie réponse.
À quelle fréquence devons-nous tester notre site web éducatif pour l'accessibilité ?
Le test automatisé doit s'exécuter sur chaque demande de tirage -- c'est à quoi sert le pipeline CI/CD. Le test de navigation au clavier manuel doit se faire avant chaque version majeure. Le test du lecteur d'écran (VoiceOver, NVDA) doit se faire au moins trimestriellement. Un audit WCAG professionnel complet doit se faire annuellement ou chaque fois que des fonctionnalités significatives sont ajoutées.
Next.js rend-il automatiquement un site web accessible ?
Aucun framework n'est automatiquement accessible -- vous devez toujours écrire un bon code. Mais Next.js offre des avantages significatifs : le composant Image vous avertit du texte alt manquant, le composant Link génère des bonnes balises <a> avec une gestion correcte du href, JSX de React encourage les éléments sémantiques, et le pipeline de compilation supporte le test automatisé d'accessibilité. Le framework ne fait pas le travail pour vous, mais il rend faire la bonne chose le chemin de moindre résistance.
Quelles sont les pénalités pour avoir un site web universitaire inaccessible ?
Les règlements ADA pour les sites web éducatifs ont varié de 25 000 $ à plus de 300 000 $, plus les honoraires d'avocat, plus le coût de la correction (qui peut dépasser le règlement lui-même). Au-delà des pénalités monétaires, l'OCR peut exiger des accords de conformité qui obligent à surveiller et rapporter en continu pendant des années. Et il y a les dommages à la réputation -- le genre qui fait que les étudiants et les professeurs prospectifs réfléchissent à deux fois avant de s'engager auprès de votre établissement.