← Alle Beiträge

API & Helfer

Ein Live-Barrierefreiheits-Audit-Overlay bauen, das du überall injizieren kannst

Lighthouse und axe sind ausgezeichnet — und sie sind auch ein Kontextwechsel. Du öffnest die DevTools, führst das Audit aus, liest den Bericht, springst zurück zur Seite. Für einen schnellen ersten Durchgang während des Bauens willst du die Probleme auf der Seite selbst gezeichnet haben: eine rote Umrandung um jedes Bild ohne Alt-Text, ein Abzeichen über jedem unbeschrifteten Eingabefeld. Dieser Artikel baut genau das als JavaScript-Regel, die du mit JustZix in jede Seite injizieren kannst.

Was dieses Overlay ist — und was es nicht ist

Das ist ein Erste-Durchgang-Schnelltest. Es fängt die billigen, häufigen, mechanischen Fehler ab — die, die einen großen Anteil echter Barrierefreiheits-Mängel ausmachen — und es zeigt sie visuell, sodass du sie nicht übersehen kannst. Es ist kein Ersatz für axe-core oder Lighthouse: Es testet ARIA-Semantik nicht tief, ebenso wenig die Fokus-Reihenfolge oder das Verhalten von Screenreadern. Sieh es als den Linter, den du ständig laufen lässt, nicht das volle Audit, das du vor dem Ausliefern machst.

Das Ganze ist eine JS-Regel. Injiziere sie, wirf einen Blick auf die Seite, behebe, was aufleuchtet, injiziere neu.

Das Skelett

Wir sammeln Probleme in einem Array, zeichnen eine Umrandung plus ein Abzeichen für jedes und zeigen einen Zusammenfassungszähler. Fang mit dem Gerüst an.

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

Ein flag()-Aufruf pro Problemelement. Es erfasst das Problem und malt eine absolut positionierte Box über das Element mit einem beschrifteten Abzeichen. Alles trägt die Klasse jz-a11y, damit der nächste Lauf es wegwischen kann.

Prüfung 1 — Bilder ohne Alt-Text

Ein <img> ganz ohne alt-Attribut ist ein Fehler; alt="" ist gültig (es markiert das Bild als dekorativ), also markieren wir nur ein wirklich fehlendes Attribut.

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

Prüfung 2 — Formularfelder ohne Label

Ein Eingabefeld braucht einen barrierefreien Namen. Der kann von einem umhüllenden oder zugeordneten <label>, einem aria-label oder einem aria-labelledby kommen. Ist keines vorhanden, ist das Feld mit einem Screenreader unbenutzbar.

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

Prüfung 3 — leere Links und Buttons

Ein Link oder Button ohne Textinhalt und ohne barrierefreies Label wird nur als „Link" oder „Button" angesagt. Das ist extrem häufig bei reinen Icon-Buttons.

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

Prüfung 4 — Text mit niedrigem Kontrast

Die volle WCAG-Kontrast-Mathematik ist aufwendig, aber eine Prüfung der relativen Leuchtdichte fängt die schlimmsten Übeltäter. Berechne die Leuchtdichte für die Textfarbe und den aufgelösten Hintergrund, dann nimm das Kontrastverhältnis.

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

Die Schwelle von 4,5:1 ist das WCAG-AA-Minimum für Text in normaler Größe. Großer Text bekommt bei 3:1 grünes Licht, also rechne mit ein paar Fehlalarmen bei großen Überschriften — die schau dir mit dem Auge an.

Prüfung 5 — Sprünge in der Überschriften-Reihenfolge und fehlendes lang

Überschriften sollten keine Ebenen überspringen — eine <h2>, direkt gefolgt von einer <h4>, bricht die Dokumentgliederung. Und das Wurzelelement <html> braucht ein lang, damit Screenreader die richtige Aussprache wählen.

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

Der Zusammenfassungszähler

Schließlich ein fixes Abzeichen in der Ecke mit der Gesamtzahl. Es ist der Auf-einen-Blick-Wert; die Umrandungen sind das Detail.

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

Das console.table(issues) am Ende gibt außerdem eine ordentliche Liste in die Output Console aus, sodass du sowohl das visuelle Overlay als auch eine kopierbare Zusammenfassung hast.

Wie du es im Alltag nutzt

Siehe auch

Ein Live-Audit-Overlay macht aus Barrierefreiheit statt einer Checkliste, die du vergisst, etwas, das du nicht ignorieren kannst. Installiere JustZix, füge das Skript ein und lass die Seite dir sagen, was falsch ist.

Bewerte diesen Beitrag

Noch keine Bewertungen — sei der Erste.

Probiere es selbst aus

Installiere JustZix und füge ein beliebiges Snippet aus diesem Artikel ein. Zwei Minuten von null bis zu einer funktionierenden Regel auf allen deinen Geräten.

JustZix holen

Funktionen · So funktioniert es · Beispiele · Anwendungsfälle