Warum ein deaktivierter Button keinen Tooltip auslösen kann, wie ein umschliessendes Element das löst und was genau nötig ist, damit auch ein Screenreader die Erklärung vorliest, liest du in diesem Beitrag.

Wenn ein Button deaktiviert ist

In einem Kundenprojekt fand ich Buttons vor, die je nach Situation deaktiviert sind – und deren Icon nicht auf Anhieb verrät, wofür sie da sind. Genau dafür haben sie normalerweise einen Tooltip. Nur: Sobald der Button deaktiviert ist, erscheint der Tooltip nicht mehr. Damit fehlt die Erklärung genau in dem Moment, in dem man sie am nötigsten braucht – wenn man wissen will, was der Button macht und warum er gerade nicht zur Verfügung steht.

Wofür ich Tooltips einsetze und was passiert, wenn sie einem Modal in die Quere kommen, habe ich im Beitrag Modal oder nicht – das ist hier nicht die Frage beschrieben. Beim Like-Button übernimmt ein manuell gesteuerter Tooltip sogar das Feedback.

Hinweis: Die Code-Beispiele setzen Bootstrap 5.3 voraus, inklusive der Tooltips, die in Bootstrap selbst initialisiert werden müssen. Sie sind zur Erklärung vereinfacht.

Warum zeigt ein deaktivierter Button keinen Tooltip?

Weil ein Element mit dem Attribut disabled nicht mehr interaktiv ist: Es lässt sich weder fokussieren noch anklicken und reagiert nicht auf die Maus. Ein Tooltip in Bootstrap erscheint aber genau dann, wenn das Element den Fokus bekommt oder die Maus darüberfährt. Beides fällt weg.

Bootstrap geht dabei bewusst noch einen Schritt weiter: Deaktivierte Buttons bekommen pointer-events: none, damit weder Hover- noch Active-Zustand greifen. Bei Links ist es dasselbe, nur auf anderem Weg. Ein <a>-Tag kennt kein disabled-Attribut, in Bootstrap wird es über die CSS-Klasse .disabled deaktiviert – ebenfalls mit pointer-events: none – und bekommt, falls es sein href behält, zusätzlich tabindex="-1", damit es auch per Tastatur nicht mehr erreichbar ist.

Das ist richtig so: Ein deaktivierter Button soll nicht bedienbar sein. Nur verschwindet mit der Bedienbarkeit auch jede Möglichkeit, etwas über den Button zu sagen.

Merke: Bei einem <button> reicht das Attribut disabled, die CSS-Klasse .disabled braucht es nur bei Links. Bootstrap gestaltet beides gleich. Ein Button nur mit der CSS-Klasse sieht aber bloss deaktiviert aus: pointer-events: none hält die Maus ab, per Tastatur lässt er sich weiterhin erreichen und auslösen – und ein Screenreader erfährt nicht, dass er deaktiviert ist.

Wie bekommt der deaktivierte Button trotzdem einen Tooltip?

Über ein umschliessendes Element, das den Tooltip an seiner Stelle trägt.

In der Bootstrap-Dokumentation wird dafür ein <span> oder <div> vorgeschlagen, das mit tabindex="0" per Tastatur fokussierbar wird.

<span class="d-inline-block"
      tabindex="0"
      data-bs-toggle="tooltip"
      title="Erst nach dem Speichern möglich">
    <button class="btn btn-primary" type="button" disabled>
        <span class="icon" aria-hidden="true">icon</span> Exportieren
    </button>
</span>

Mit der Maus löst jetzt der span den Tooltip aus: Der Button ist durch das disabled-Attribut deaktiviert und dank pointer-events: none für die Maus gar nicht erreichbar. Mit der Tastatur übernimmt das tabindex="0" – der span bekommt den Fokus, den der Button nicht mehr bekommen kann. Die CSS-Klasse d-inline-block sorgt dafür, dass der span genau so gross ist wie der Button und der Tooltip dort sitzt, wo er hingehört.

Den Text für den Tooltip nimmt Bootstrap aus zwei Quellen: aus data-bs-title, wie in der Dokumentation, oder direkt aus dem title-Attribut. Im Kundenprojekt habe ich title direkt genommen. Denn Bootstrap entfernt das title-Attribut beim Initialisieren und merkt sich den Text selbst – so erscheint nicht zusätzlich noch der native Tooltip des Browsers.

Für Maus und Tastatur ist das Problem damit gelöst. Für Screenreader noch nicht.

Was ist ein Screenreader überhaupt?

Ein Screenreader liest vor, was auf dem Bildschirm steht, und macht Computer, Tablet und Mobile so ohne Blick auf den Bildschirm bedienbar – gebraucht wird er vor allem von blinden und sehbehinderten Menschen. «Screenreader» ist dabei der Oberbegriff, VoiceOver das konkrete, in macOS und iOS eingebaute Werkzeug. Unter Windows heissen die verbreiteten Vertreter NVDA und JAWS, unter Android TalkBack.

Was liest der Screenreader vor?

Ohne weitere Angaben nichts Verlässliches.

Die Bootstrap-Dokumentation weist selbst darauf hin, dass die meisten Hilfstechnologien einen Tooltip auf einem Element, das nur über tabindex="0" fokussierbar wird, nicht ansagen. Der span hat weder eine Rolle noch einen Namen – der Fokus landet also auf einem Element, das nichts über sich sagt. Ein solches fokussierbares Nichts hatte ich schon einmal gebaut, bei meiner klickbaren Beitragsvorschau.

Damit der Screenreader vorliest, was der Tooltip zeigt, benötigt es weitere Angaben, und zwar drei auf dem span und eine auf dem Button:

<span class="d-inline-block"
      role="button"
      aria-disabled="true"
      tabindex="0"
      aria-label="Exportieren"
      data-bs-toggle="tooltip"
      title="Erst nach dem Speichern möglich">
    <button class="btn btn-primary" type="button" disabled aria-hidden="true">
        <span class="icon" aria-hidden="true">icon</span>
        <span>Exportieren</span>
    </button>
</span>

Was die Angaben bewirken

Für den Screenreader macht role="button" aus dem span einen Button. Über aria-disabled="true" wird dieser Button als deaktiviert gekennzeichnet – anders als disabled lässt es den span aber fokussierbar, und genau das braucht der Tooltip. Das aria-label legt fest, was der Screenreader für den span vorliest: den sichtbaren Text des umschlossenen disabled-Buttons. In der Fachsprache heisst dieser vorgelesene Text «Accessible Name», auf Deutsch «zugänglicher Name». Der sichtbare Text muss dort hinein, weil das aria-label den Inhalt des span ersetzt – ohne ihn würde der Screenreader «Exportieren» nie vorlesen. Die WCAG verlangen das ohnehin: Der sichtbare Text muss im zugänglichen Namen stecken (2.5.3 Label in Name). Bei einem Button, der nur ein Icon zeigt, steht an dieser Stelle der Name der Funktion.

Die Erklärung darf nicht ins aria-label, sondern bleibt im Tooltip. Bootstrap hängt ihn beim Einblenden über aria-describedby an den span und der Screenreader liest ihn als Beschreibung nach dem Namen vor. Steht die Erklärung an beiden Stellen, hört man sie zweimal – genau das zeigt ein Test mit dem Screenreader.

Der Button erhält das Attribut aria-hidden="true", damit der Screenreader ihn nicht ein zweites Mal findet – einmal über den span und einmal als deaktivierten Button darin. Auf einem fokussierbaren Element wäre aria-hidden heikel, hier ist der Button durch disabled ohnehin aus der Tab-Reihenfolge genommen. Das aria-hidden auf dem Icon darf dabei bleiben: Es stört nicht und man muss bestehenden Code nicht zwingend verändern.

Bei einem deaktivierten Link gilt dasselbe wie gerade beschrieben: aria-hidden="true" muss nur auf den Link gesetzt werden.

Die Rolle, die zuerst fehlte

Die Rolle hatte ich im Kundenprojekt ursprünglich nicht gesetzt. Beim Nachrecherchieren für diesen Beitrag hat sich gezeigt, dass ein aria-label auf einem span ohne Rolle gar nicht vorgesehen ist: Ein span hat von sich aus die Rolle generic und für diese Rolle schliesst die ARIA-Spezifikation das aria-label ausdrücklich aus. Erst mit role="button" steht der Name auch formal dort, wo er benötigt wird.

Anmerkung: Statt mit dem umschliessenden Element könnte man den Button auch nur mit aria-disabled="true" statt disabled deaktivieren. Dann bleibt er fokussierbar und der Tooltip sitzt direkt auf ihm. Den Klick muss man dann aber selbst per JavaScript unterbinden, denn aria-disabled sagt nur an, dass der Button deaktiviert ist – verhindern tut es nichts. Und wenn man für das Aussehen die CSS-Klasse .disabled ergänzt, hat man mit ihrem pointer-events: none den Tooltip für die Maus gleich wieder verloren. Daher ist diese Variante weniger stabil und verursacht mehr Aufwand – ich würde sie nicht empfehlen.

Wie testet man so eine Lösung auf dem Mac?

Dafür braucht es keine zusätzliche Software, VoiceOver ist auf jedem Mac dabei.

  1. In den Systemeinstellungen unter «Tastatur» die Tastaturnavigation einschalten, sonst springt der Tabulator in Safari nur zwischen Formularfeldern.
  2. VoiceOver mit Cmd + F5 starten.
  3. Das Beschriftungsfenster zeigt das Gesprochene als Text. Oft ist es bereits eingeblendet, ist es das nicht, kann man es mit der Tastenkombination Ctrl + Option + Cmd + F10 hervorholen. Dieselbe Tastenkombination blendet es auch wieder aus.
  4. Mit dem Tabulator auf den span gehen und mitlesen, was angesagt wird und was doppelt kommt.
  5. VoiceOver mit Cmd + F5 wieder beenden.

Die ehrliche Grenze

Getestet habe ich die Lösung des umschliessenden span mit dem Tooltip auf dem Mac mit VoiceOver, in Safari und in Chrome. Angesagt werden dort der zugängliche Name, die Erklärung aus dem Tooltip und der Zustand «dimmed» für aria-disabled. Mit NVDA oder JAWS unter Windows habe ich sie nicht geprüft, und wie viel ein Screenreader vom deaktivierten Zustand überhaupt ansagt, ist von Programm zu Programm verschieden. Testen ist daher wichtig und Teil der Umsetzung, das richtig geschriebene Markup allein reicht nicht aus.

Warum der Aufwand für einen Button, der nichts macht?

Weil der Button zwar nichts macht, aber trotzdem etwas mitteilt. Er zeigt, dass es eine Funktion gibt, dass sie gerade nicht zur Verfügung steht und – mit dem Tooltip – warum. Wer den Button sieht, bekommt diese Information über den Tooltip. Wer einen Screenreader nutzt, sollte dieselbe Information bekommen – und bekommt sie mit dieser Lösung.

Barrierefreiheit wird zudem von der Kür zur Grundanforderung, den rechtlichen Rahmen habe ich im Abschnitt Barrierefreiheit meines Beitrags zum Like-Button beschrieben. Und sie lässt sich auch hier richtig umsetzen – ohne zusätzliches JavaScript, nur mit ein paar Attributen im Markup. Wie oft die zugängliche Lösung zugleich die schlankere ist, habe ich schon in Weniger Code, mehr Barrierefreiheit festgestellt.

Fazit

Ein deaktivierter Button ist nicht bedienbar – aber er ist da und er steht dort aus einem Grund. Wer ihn sieht, fragt sich, wofür er da ist und warum er gerade nicht funktioniert. Diese Frage verschwindet nicht mit dem disabled-Attribut, sie wird eher dringlicher. Mit dem Umweg über ein umschliessendes Element bekommt die Frage eine Antwort, und zwar für alle, ob per Maus, Tastatur oder Screenreader. Deaktiviert darf ein Button sein – unerklärt muss er deshalb nicht bleiben.