Crear una capa de auditoría de accesibilidad en vivo que puedes inyectar en cualquier sitio
Lighthouse y axe son excelentes — y también son un cambio de contexto. Abres DevTools, ejecutas la auditoría, lees el informe, vuelves a la página. Para una primera pasada rápida mientras construyes, quieres los problemas dibujados sobre la propia página: un contorno rojo alrededor de cada imagen sin texto alt, una insignia sobre cada campo sin etiqueta. Este artículo construye exactamente eso como una regla JavaScript que puedes inyectar en cualquier sitio con JustZix.
Qué es esta capa — y qué no es
Esto es un test de humo de primera pasada. Detecta los errores baratos, comunes y mecánicos — los que representan una gran parte de los fallos reales de accesibilidad — y los muestra visualmente para que no puedas pasarlos por alto. No es un sustituto de axe-core o Lighthouse: no comprueba la semántica ARIA en profundidad, el orden del foco ni el comportamiento del lector de pantalla. Piénsalo como el linter que ejecutas constantemente, no la auditoría completa que ejecutas antes de publicar.
Todo el conjunto es una regla JS. Inyéctala, echa un vistazo a la página, arregla lo que se ilumine, reinyecta.
El esqueleto
Recogeremos los problemas en un array, dibujaremos un contorno más una insignia por cada uno y mostraremos un contador resumen. Empieza con el andamiaje.
(() => {
// Clean up a previous run
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; // skip hidden
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);
}
Una llamada a flag() por cada elemento problemático. Registra el problema y pinta una caja posicionada de forma absoluta sobre el elemento con una insignia etiquetada. Todo lleva la clase jz-a11y para que la siguiente ejecución pueda borrarlo.
Comprobación 1 — imágenes sin alt
Una <img> sin ningún atributo alt es un fallo; alt="" es válido (marca la imagen como decorativa), así que solo señalamos un atributo genuinamente ausente.
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('alt')) {
flag(img, 'img: no alt', '#e11d48');
}
});
Comprobación 2 — campos de formulario sin etiqueta
Un campo necesita un nombre accesible. Eso puede venir de un <label> envolvente o asociado, de un aria-label o de aria-labelledby. Si no hay ninguno, el campo es inutilizable con un lector de pantalla.
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');
}
});
Comprobación 3 — enlaces y botones vacíos
Un enlace o botón sin contenido de texto y sin etiqueta accesible se anuncia simplemente como «enlace» o «botón». Esto es extremadamente común con los botones de solo icono.
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');
}
});
Comprobación 4 — texto con contraste bajo
Las matemáticas completas de contraste de WCAG son complejas, pero una comprobación de luminancia relativa detecta a los peores infractores. Calcula la luminancia para el color del texto y el fondo resuelto, y luego toma la 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');
}
});
El umbral de 4.5:1 es el mínimo WCAG AA para texto de tamaño normal. El texto grande pasa con 3:1, así que espera algún falso positivo en encabezados grandes — revísalos a ojo.
Comprobación 5 — saltos en el orden de encabezados y lang ausente
Los encabezados no deberían saltarse niveles — un <h2> seguido directamente de un <h4> rompe el esquema del documento. Y el <html> raíz necesita un lang para que los lectores de pantalla elijan la pronunciación correcta.
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' });
}
El contador resumen
Por último, una insignia fija en la esquina con el total. Es la puntuación de un vistazo; los contornos son el detalle.
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);
})();
El console.table(issues) del final también vuelca una lista ordenada a la Consola de Salida, así que tienes tanto la capa visual como un resumen copiable.
Cómo usarla en el día a día
- Coloca el script entero en una regla JS acotada al sitio que estás construyendo, y ejecútala bajo demanda desde el panel JS (Ctrl+Enter) para que no se redibuje en cada navegación.
- Los contornos están posicionados de forma absoluta, así que tras un redimensionado grande basta con volver a ejecutarla.
- Arregla primero los problemas más ruidosos — el alt ausente y los campos sin etiqueta suelen ser arreglos HTML de una línea.
- Cuando la insignia de la esquina se ponga verde, cambia a Lighthouse o axe para la auditoría profunda. Esta capa despeja el 80% fácil; las herramientas reales gestionan el resto.
Mira también
- Una capa de depuración responsive — la misma idea aplicada a bugs de maquetación.
- Desactivar patrones oscuros y urgencia falsa — arreglar otro tipo de UX rota.
Una capa de auditoría en vivo convierte la accesibilidad de una lista de comprobación que olvidas en algo que no puedes ignorar. Instala JustZix, pega el script y deja que la página te diga qué está mal.
Valora este artículo
Sin valoraciones — sé el primero.