← Tutti gli articoli

API e helper

Costruire un overlay di audit di accessibilita dal vivo da iniettare ovunque

Lighthouse e axe sono eccellenti — e sono anche un cambio di contesto. Apri i DevTools, esegui l'audit, leggi il report, torni alla pagina. Per una rapida prima verifica mentre costruisci, vuoi i problemi disegnati sulla pagina stessa: un contorno rosso intorno a ogni immagine senza testo alt, un distintivo su ogni input senza etichetta. Questo articolo costruisce esattamente quello come una regola JavaScript che puoi iniettare in qualsiasi sito con JustZix.

Cos'e questo overlay — e cosa non e

Questa e una verifica rapida di prima passata. Coglie gli errori economici, comuni e meccanici — quelli che rappresentano una grande fetta dei reali fallimenti di accessibilita — e li mostra visivamente cosi non puoi mancarli. Non e un sostituto di axe-core o Lighthouse: non testa a fondo la semantica ARIA, l'ordine di focus o il comportamento dello screen reader. Pensalo come il linter che esegui costantemente, non l'audit completo che esegui prima del rilascio.

Il tutto e un'unica regola JS. Iniettala, dai un'occhiata alla pagina, correggi cio che si illumina, re-inietta.

Lo scheletro

Raccoglieremo i problemi in un array, disegneremo un contorno piu un distintivo per ciascuno e mostreremo un contatore riepilogativo. Inizia con l'impalcatura.

(() => {
  // 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 chiamata a flag() per ogni elemento problematico. Registra il problema e dipinge un box posizionato in modo assoluto sopra l'elemento con un distintivo etichettato. Tutto porta la classe jz-a11y cosi l'esecuzione successiva puo cancellarlo.

Controllo 1 — immagini senza alt

Un <img> senza alcun attributo alt e un fallimento; alt="" e valido (segna l'immagine come decorativa), quindi segnaliamo solo un attributo davvero mancante.

  document.querySelectorAll('img').forEach(img => {
    if (!img.hasAttribute('alt')) {
      flag(img, 'img: no alt', '#e11d48');
    }
  });

Controllo 2 — input di modulo senza etichetta

Un input ha bisogno di un nome accessibile. Puo venire da un <label> che lo avvolge o associato, da un aria-label, o da aria-labelledby. Se nessuno e presente, il campo e inutilizzabile con uno screen reader.

  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');
    }
  });

Controllo 3 — link e pulsanti vuoti

Un link o un pulsante senza contenuto testuale e senza etichetta accessibile viene annunciato semplicemente come "link" o "pulsante". E estremamente comune con i pulsanti con sola icona.

  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');
    }
  });

Controllo 4 — testo a basso contrasto

La matematica completa del contrasto WCAG e complicata, ma un controllo di luminanza relativa coglie i peggiori trasgressori. Calcola la luminanza per il colore del testo e per lo sfondo risolto, poi prendi il rapporto di contrasto.

  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');
      }
    });

La soglia di 4,5:1 e il minimo WCAG AA per il testo di dimensione normale. Il testo grande passa a 3:1, quindi aspettati qualche falso positivo sui titoli grandi — valutali a occhio.

Controllo 5 — salti nell'ordine dei titoli e lang mancante

I titoli non dovrebbero saltare livelli — un <h2> seguito direttamente da un <h4> rompe la struttura del documento. E la radice <html> ha bisogno di un lang cosi gli screen reader scelgono la pronuncia giusta.

  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' });
  }

Il contatore riepilogativo

Infine, un distintivo fisso nell'angolo con il totale. E il punteggio a colpo d'occhio; i contorni sono il dettaglio.

  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);
})();

Il console.table(issues) alla fine scarica anche un elenco ordinato nella Output Console, cosi hai sia l'overlay visivo sia un riepilogo copiabile.

Come usarlo giorno per giorno

Vedi anche

Un overlay di audit dal vivo trasforma l'accessibilita da una checklist che dimentichi a qualcosa che non puoi ignorare. Installa JustZix, incolla lo script e lascia che sia la pagina a dirti cosa c'e di sbagliato.

Valuta questo articolo

Nessuna valutazione — sii il primo.

Provalo tu stesso

Installa JustZix e incolla qualsiasi snippet di questo articolo. Due minuti da zero a una regola funzionante su tutti i tuoi dispositivi.

Ottieni JustZix

Funzionalità · Come funziona · Esempi · Casi d'uso