Construire une surcouche d'audit d'accessibilité en direct injectable partout
Lighthouse et axe sont excellents — et ce sont aussi un changement de contexte. Vous ouvrez les DevTools, lancez l'audit, lisez le rapport, revenez à la page. Pour une première passe rapide pendant que vous construisez, vous voulez les problèmes dessinés sur la page elle-même : un contour rouge autour de chaque image sans texte alternatif, un badge sur chaque champ non étiqueté. Cet article construit exactement cela sous forme de règle JavaScript que vous pouvez injecter dans n'importe quel site avec JustZix.
Ce qu'est cette surcouche — et ce qu'elle n'est pas
C'est un test rapide de première passe. Il attrape les erreurs bon marché, courantes et mécaniques — celles qui représentent une grande part des vrais échecs d'accessibilité — et il les montre visuellement pour que vous ne puissiez pas les manquer. Ce n'est pas un remplacement d'axe-core ou de Lighthouse : il ne teste pas la sémantique ARIA en profondeur, l'ordre du focus, ni le comportement des lecteurs d'écran. Pensez-y comme au linter que vous lancez constamment, pas à l'audit complet que vous lancez avant de livrer.
Le tout est une seule règle JS. Injectez-la, jetez un œil à la page, corrigez ce qui s'allume, ré-injectez.
Le squelette
Nous allons collecter les problèmes dans un tableau, dessiner un contour plus un badge pour chacun, et afficher un compteur récapitulatif. Commencez par l'ossature.
(() => {
// Nettoie une execution precedente
document.querySelectorAll('.jz-a11y').forEach(n => n.remove());
const issues = [];
function flag(el, label, color) {
if (!el || !el.getBoundingClientRect) return;
const r = el.getBoundingClientRect();
if (r.width === 0 && r.height === 0) return; // ignore les caches
issues.push({ label });
const box = document.createElement('div');
box.className = 'jz-a11y';
Object.assign(box.style, {
position: 'absolute',
left: (r.left + scrollX) + 'px',
top: (r.top + scrollY) + 'px',
width: r.width + 'px',
height: r.height + 'px',
outline: '2px solid ' + color,
background: color + '22',
zIndex: 2147483000,
pointerEvents: 'none',
boxSizing: 'border-box',
});
const tag = document.createElement('span');
tag.textContent = label;
Object.assign(tag.style, {
position: 'absolute', left: 0, top: '-18px',
font: '11px/1.4 system-ui, sans-serif',
background: color, color: '#fff',
padding: '1px 5px', whiteSpace: 'nowrap',
});
box.appendChild(tag);
document.body.appendChild(box);
}
Un appel à flag() par élément problématique. Il enregistre le problème et peint une boîte positionnée en absolu sur l'élément avec un badge étiqueté. Tout porte la classe jz-a11y pour que l'exécution suivante puisse l'effacer.
Vérification 1 — images sans alt
Une <img> sans aucun attribut alt est un échec ; alt="" est valide (il marque l'image comme décorative), donc nous ne signalons qu'un attribut véritablement manquant.
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('alt')) {
flag(img, 'img: no alt', '#e11d48');
}
});
Vérification 2 — champs de formulaire sans étiquette
Un champ a besoin d'un nom accessible. Cela peut venir d'un <label> englobant ou associé, d'un aria-label, ou d'un aria-labelledby. Si aucun n'est présent, le champ est inutilisable avec un lecteur d'écran.
const fields = 'input:not([type=hidden]):not([type=submit])'
+ ':not([type=button]), select, textarea';
document.querySelectorAll(fields).forEach(el => {
const byFor = el.id &&
document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
const wrapped = el.closest('label');
const aria = el.getAttribute('aria-label')
|| el.getAttribute('aria-labelledby')
|| el.getAttribute('title');
if (!byFor && !wrapped && !aria) {
flag(el, 'input: no label', '#f59e0b');
}
});
Vérification 3 — liens et boutons vides
Un lien ou un bouton sans contenu textuel ni étiquette accessible est annoncé simplement comme « lien » ou « bouton ». C'est extrêmement courant avec les boutons à icône seule.
document.querySelectorAll('a, button').forEach(el => {
const text = (el.textContent || '').trim();
const aria = el.getAttribute('aria-label')
|| el.getAttribute('title');
const img = el.querySelector('img[alt]:not([alt=""])');
const hasName = text || aria || img;
if (!hasName) {
const what = el.tagName === 'A' ? 'link' : 'button';
flag(el, 'empty ' + what, '#8b5cf6');
}
});
Vérification 4 — texte à faible contraste
Le calcul complet du contraste WCAG est compliqué, mais une vérification de luminance relative attrape les pires fautifs. Calculez la luminance pour la couleur du texte et le fond résolu, puis prenez le ratio de contraste.
function lum(rgb) {
const c = rgb.map(v => {
v /= 255;
return v <= 0.03928
? v / 12.92
: Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126*c[0] + 0.7152*c[1] + 0.0722*c[2];
}
function parse(str) {
const m = str.match(/\d+(\.\d+)?/g);
return m ? m.slice(0, 3).map(Number) : null;
}
function bgOf(el) {
let n = el;
while (n) {
const bg = getComputedStyle(n).backgroundColor;
if (bg && !/rgba?\(0, 0, 0, 0\)|transparent/.test(bg)) {
return parse(bg);
}
n = n.parentElement;
}
return [255, 255, 255];
}
document.querySelectorAll('p, span, a, li, h1, h2, h3, h4, td')
.forEach(el => {
if (!(el.textContent || '').trim()) return;
const fg = parse(getComputedStyle(el).color);
const bg = bgOf(el);
if (!fg || !bg) return;
const l1 = lum(fg) + 0.05, l2 = lum(bg) + 0.05;
const ratio = l1 > l2 ? l1 / l2 : l2 / l1;
if (ratio < 4.5) {
flag(el, 'contrast ' + ratio.toFixed(1) + ':1', '#0ea5e9');
}
});
Le seuil de 4,5:1 est le minimum WCAG AA pour le texte de taille normale. Le grand texte passe à 3:1, alors attendez-vous à quelques faux positifs sur les grands titres — vérifiez-les à l'œil.
Vérification 5 — sauts dans l'ordre des titres et lang manquant
Les titres ne devraient pas sauter de niveaux — un <h2> suivi directement d'un <h4> casse le plan du document. Et la racine <html> a besoin d'un lang pour que les lecteurs d'écran choisissent la bonne prononciation.
let prev = 0;
document.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(h => {
const level = +h.tagName[1];
if (prev && level > prev + 1) {
flag(h, 'jump h' + prev + '→h' + level, '#db2777');
}
prev = level;
});
if (!document.documentElement.getAttribute('lang')) {
issues.push({ label: 'html: no lang' });
}
Le compteur récapitulatif
Enfin, un badge fixe dans le coin avec le total. C'est le score d'un coup d'œil ; les contours sont le détail.
const sum = document.createElement('div');
sum.className = 'jz-a11y';
sum.textContent = issues.length
? issues.length + ' a11y issue(s)'
: 'No quick a11y issues found';
Object.assign(sum.style, {
position: 'fixed', right: '12px', bottom: '12px',
font: '13px/1.5 system-ui, sans-serif',
background: issues.length ? '#e11d48' : '#16a34a',
color: '#fff', padding: '6px 12px', borderRadius: '6px',
zIndex: 2147483600, pointerEvents: 'none',
});
document.body.appendChild(sum);
console.table(issues);
})();
Le console.table(issues) à la fin déverse aussi une liste ordonnée dans la console de sortie, donc vous avez à la fois la surcouche visuelle et un récapitulatif copiable.
Comment l'utiliser au quotidien
- Déposez tout le script dans une règle JS ciblée sur le site que vous construisez, et exécutez-le à la demande depuis le panneau JS (Ctrl+Entrée) pour qu'il ne se redessine pas à chaque navigation.
- Les contours sont positionnés en absolu, donc après un grand redimensionnement, relancez-le simplement.
- Corrigez d'abord les problèmes les plus criants — les alt manquants et les champs non étiquetés sont généralement des corrections HTML d'une ligne.
- Quand le badge du coin passe au vert, passez à Lighthouse ou axe pour l'audit en profondeur. Cette surcouche règle les 80 % faciles ; les vrais outils gèrent le reste.
À voir aussi
- Une surcouche de débogage responsive — la même idée appliquée aux bugs de mise en page.
- Désactiver les dark patterns et la fausse urgence — corriger un autre genre d'UX cassée.
Une surcouche d'audit en direct transforme l'accessibilité d'une checklist que vous oubliez en quelque chose que vous ne pouvez pas ignorer. Installez JustZix, collez le script, et laissez la page vous dire ce qui ne va pas.
Notez cet article
Aucune note — soyez le premier.