htmx und Barrierefreiheit: was beim Austausch von HTML verloren geht
Ich bin mit der Tastatur durch vier Fassungen einer Demo gegangen: wohin der Fokus nach einem htmx-Tausch fällt und was du selbst regeln musst.
von Jean Pierre Kolb ·
In Teil 2 hat die robuste htmx-Fassung meiner Demo glänzend abgeschnitten. Jeder Inhalt steht im HTML, jeder Zustand hat eine eigene URL, und Crawler sehen alles. Für die Barrierefreiheit reicht das nicht. Eine Seite kann für Suchmaschinen vollständig sein und trotzdem für Menschen, die mit Tastatur oder Screenreader unterwegs sind, an genau den Stellen ins Leere laufen, an denen htmx HTML austauscht.
Ich bin deshalb mit der Tastatur durch alle vier Fassungen des Veranstaltungskalenders gegangen und habe nach jedem Schritt festgehalten, wo der Fokus steht, welcher Seitentitel gilt und in welcher Sprache die Seite sich ausgibt. Das Ergebnis ist eine Liste von Dingen, die htmx dir nicht abnimmt.
Methodik-Hinweis: Gemessen am 29.09.2026 gegen die live ausgelieferte Demo, in einem Headless-Chromium (
HeadlessChrome/155) über Playwright. Ich habe das jeweilige Ziel fokussiert und dann mit echter Tastatureingabe ausgelöst (Enter,Escape). Nach jedem Schritt habe ich das aktive Element, den Titel, daslang-Attribut, offene Dialoge und vorhandene Live-Regionen ausgelesen. Einen Screenreader habe ich nicht eingesetzt. Was ein Screenreader tatsächlich ansagt, leite ich aus dem DOM-Zustand und aus der Dokumentation ab, nicht aus einem Hörtest.
Was beim Austausch passiert
Wenn du einen Link anklickst, lädt der Browser ein neues Dokument. Er setzt den Fokus zurück an den Seitenanfang, übernimmt Titel und Sprache der neuen Seite, und assistive Technik bemerkt den Seitenwechsel. Nichts davon muss die Website selbst erledigen.
Tauscht htmx dagegen einen Teil der Seite aus, bleibt das Dokument dasselbe. Der Browser hat keinen Anlass, irgendetwas zu melden. Ob der Fokus sinnvoll steht, ob ein Screenreader von der Änderung erfährt und ob Titel und Sprache stimmen, hängt dann an der Bibliothek und an dir. Vier Kriterien der WCAG 2.2 (öffnet in neuem Tab) sind dabei besonders berührt:
- 2.4.3 Focus Order (Stufe A): Der Fokus muss in einer Reihenfolge wandern, die Sinn und Bedienbarkeit erhält.
- 2.4.2 Page Titled (Stufe A): Seiten brauchen einen Titel, der Thema oder Zweck beschreibt.
- 3.1.1 Language of Page (Stufe A): Die Sprache der Seite muss sich maschinell bestimmen lassen.
- 4.1.3 Status Messages (Stufe AA): Statusmeldungen müssen assistiver Technik übermittelt werden, ohne den Fokus zu verschieben.
Eine autorisierte deutsche Übersetzung gibt es nur für WCAG 2.0 (öffnet in neuem Tab). Sie enthält die ersten drei Kriterien, aber nicht 4.1.3, das erst mit WCAG 2.1 dazukam. Für WCAG 2.2 gibt es keine deutsche Fassung.
Das Ergebnis im Überblick
| Schritt | htmx (robust) | htmx (fragil) | Unpoly | ohne Bibliothek |
|---|---|---|---|---|
| Tab auslösen | Fokus → <body> | Fokus bleibt auf dem Button | Fokus → Tab-Link | neue Seite, Fokus → <body> |
| „Mehr laden" | Fokus → <body>, Titel wird „Seite 2" | lädt beim Tabben unangekündigt nach | Fokus → erster Termin der Liste | neue Seite |
| Kurzinfo öffnen | Dialog, Fokus → „Schließen", Titel wird Termin | Text erscheint, Fokus bleibt, keine Ansage | Overlay, Fokus → Overlay | Dialog, Fokus → „Schließen" |
Escape | Fokus zurück zum Auslöser, Titel bleibt Termin | – | Fokus zurück zum Auslöser | Fokus zurück zum Auslöser |
| Sprachlink „English" | lang bleibt de | lang = en | lang = en | lang = en |
Live-Regionen (aria-live, role="status") und aria-busy gab es in keiner Fassung und in keinem Zustand. Die folgenden Abschnitte gehen die Zeilen einzeln durch.
Tabs: Der Fokus fällt auf den Seitenanfang
In der robusten htmx-Fassung ersetzt ein Klick auf den Tab „Workshop" den ganzen Katalog, samt der Tab-Leiste selbst. Der Link, der eben noch den Fokus hatte, existiert danach nicht mehr, und der Fokus fällt auf <body>. Wer mit der Tastatur unterwegs ist, landet wieder ganz oben und muss sich durch Kopfzeile und Tabs zurückarbeiten.
Warum das so ist, steht im Quellcode von htmx 2.0.11. htmx stellt den Fokus nach einem Tausch nur dann wieder her, wenn das zuvor fokussierte Element eine id hatte und im neuen Inhalt ein Element mit derselben id steht. Meine Tab-Links haben keine id. Daraus folgt eine naheliegende Abhilfe, die ich aus dem Code ableite, aber in der Demo nicht gemessen habe: jedem Tab-Link eine feste id geben, zum Beispiel id="tab-workshop". Dann findet htmx das Gegenstück im neuen Katalog und setzt den Fokus darauf.
Unpoly macht das von sich aus. Bei der Navigation arbeitet es eine Liste von Fokus-Strategien ab, und nach dem Tab-Wechsel stand der Fokus wieder auf dem Link „Workshop". In der fragilen htmx-Fassung wird nur die Liste unter den Buttons getauscht, nicht die Buttons selbst, deshalb bleibt der Fokus dort stehen. Ein Screenreader erfährt in keiner der beiden htmx-Fassungen, dass sich die Liste geändert hat.
„Mehr laden": Fokus weg, Titel falsch
„Mehr laden" in der robusten htmx-Fassung hängt die Termine von Seite 2 an, und der Fokus fällt wieder auf <body>. Dabei ist mir ein zweites Problem aufgefallen, das ich ohne die Messung übersehen hätte: Der Seitentitel beginnt danach mit „Alle Veranstaltungen, Seite 2", obwohl die URL bleibt und die Seite jetzt alle zwölf Termine zeigt.
Die Ursache ist eine Voreinstellung. htmx liest den <title> aus jeder Antwort und setzt ihn, auch wenn hx-select nur einen kleinen Teil der Antwort einfügt. Abschalten lässt sich das global mit htmx.config.ignoreTitle = true oder gezielt am Element:
<a href="…/seite/2/" hx-get="…/seite/2/" hx-select="#liste > li" hx-target="#liste"
hx-swap="beforeend ignoreTitle:true" hx-select-oob="#mehr">Mehr laden</a>Unpoly lässt den Titel stehen, setzt den Fokus nach dem Anhängen aber auf den ersten Termin der ganzen Liste, also zurück an den Listenanfang oberhalb des sichtbaren Bereichs. Der nächste Tab führt wieder zum ersten Termin. Das ist besser als <body>, aber nicht das, was ich mir von einer wachsenden Liste wünsche: Dort sollte der Fokus auf dem ersten neuen Termin landen, bei Unpoly etwa über das Attribut up-focus. Das habe ich in der Demo nicht ausprobiert.
Die fragile Fassung hat ein anderes Problem. Ihr „Mehr laden" löst per hx-trigger="revealed" aus, sobald das Listenende sichtbar wird. Mit der Tastatur passiert das nebenbei: Als ich mit Tab bei den letzten Terminen ankam, scrollte der Browser, der Platzhalter rückte ins Bild, und sechs neue Termine erschienen unter dem Fokus. Angesagt wurde das nicht. Es gibt keine Live-Region, und nichts zeigt an, dass die Liste gewachsen ist. (Mein erster Messdurchgang hatte hier das Gegenteil ergeben. Der Faktencheck zeigte, dass meine Sonde den Fokus gar nicht gesetzt hatte. Mit echtem Tabben ist das Ergebnis eindeutig.)
Die Kurzinfo: <dialog> rettet die Lage, der Titel nicht
Bei der Kurzinfo zeigt sich, was ein natives Element wert ist. Die robuste htmx-Fassung öffnet ein <dialog> per showModal(), die Fassung ohne Bibliothek ganz ohne Skript per command="show-modal". Beide öffnen es also als modalen Dialog, und den Rest erledigt der Browser. Laut HTML-Spezifikation (öffnet in neuem Tab) wandert der Fokus in den Dialog, in meiner Demo auf „Schließen", das erste fokussierbare Element. Der Rest der Seite wird inert, ist also weder fokussierbar noch für assistive Technik sichtbar. Escape schließt den Dialog, und der Fokus kehrt zu dem Element zurück, das ihn geöffnet hat. Genau so habe ich es in beiden Fassungen gemessen. Laut Quellcode setzen Chromium, Gecko und WebKit diese Fokus-Rückgabe um.
htmx trägt dazu nichts bei, außer dass es den Inhalt lädt. Und es fügt einen Fehler hinzu: Die Detailseite, aus der die Kurzinfo stammt, hat einen eigenen <title>, und htmx übernimmt ihn. Nach dem Öffnen beginnt der Seitentitel mit „Krimiabend mit Lokalautorin", und nach dem Schließen immer noch. Die Abhilfe ist dieselbe wie oben: ignoreTitle:true im hx-swap des Kurzinfo-Links.
Unpolys Overlay kommt ohne <dialog> aus und bringt trotzdem alles mit: role="dialog", aria-modal, einen Fokus, der im Overlay gefangen bleibt, einen Schließen-Knopf mit Namen und die Rückgabe des Fokus an den Auslöser. Auch das habe ich so gemessen. Ein Detail fiel dabei auf: Der Name des Schließen-Knopfs lautet auch auf der deutschen Seite „Dismiss dialog“. Wer Unpoly auf Deutsch einsetzt, sollte ihn übersetzen.
Die fragile htmx-Fassung zeigt, wie es ohne all das aussieht. Der Kurzinfo-Text erscheint in einem <div> unter der Liste. Der Fokus bleibt auf dem Button, und es gibt keine Live-Region. Ein Screenreader hat keinen Anlass, den neuen Text vorzulesen. Das ist ein Fall für WCAG 4.1.3.
Wenn du so ein Muster brauchst, gehört die Live-Region von Anfang an ins Markup. MDN (öffnet in neuem Tab) empfiehlt ausdrücklich, die Region leer anzulegen und erst später zu füllen, weil eine Region, die zusammen mit ihrem Inhalt eingefügt wird, oft nicht angesagt wird:
<div id="kurzinfo" aria-live="polite"></div>htmx füllt dann nur den Inhalt der bestehenden Region, und die Ansage kann greifen. In der Demo habe ich das bewusst nicht eingebaut, weil die fragile Fassung den typischen ersten Wurf zeigen soll.
Detailseite und Sprache: Was hx-boost nicht mitnimmt
Mit hx-boost lädt htmx die Detailseite per Ajax und ersetzt den Inhalt von <body>. Den Titel übernimmt es, den Fokus setzt es auf <body>, ähnlich wie ein echter Seitenwechsel. Unpoly setzt den Fokus in diesem Fall auf <main>, also auf den Inhalt statt auf die Kopfzeile.
Die deutlichste Lücke zeigte der Sprachlink. Ich habe auf der deutschen htmx-Seite „English" gewählt. URL, Titel und Inhalt waren danach englisch, aber <html lang="de"> blieb stehen. Das ist kein Messfehler: htmx übernimmt bei hx-boost nur den Inhalt von <body> und den <title>. Die Attribute des <html>-Elements fasst es nicht an, auch nicht mit der Erweiterung head-support. Screenreader, die ihre Stimme nach der Sprachauszeichnung wählen, lesen den englischen Text dann mit deutscher Aussprache vor, und die Seite verletzt WCAG 3.1.1.
Die Abhilfe ist schlicht. Der Sprachwechsel ist ein echter Seitenwechsel und sollte auch einer bleiben:
<a href="/db/en/demo/event-calendar/htmx/" hreflang="en" lang="en" hx-boost="false">English</a>hx-boost="false" nimmt den Link vom Boost aus, und der Browser lädt die englische Seite komplett mit korrektem lang. In der fragilen Fassung, die kein hx-boost nutzt, und bei Unpoly stimmte die Sprache nach dem Wechsel.
Deine Stellschrauben in htmx
Zusammengefasst hast du in htmx 2 diese Werkzeuge:
- Fokus: feste
ids an Elementen, die nach einem Tausch wieder fokussiert sein sollen. Für eigene Fokus-Logik gibt es Events wiehtmx:afterSettle. Mit dem Modifierfocus-scrollimhx-swaplegst du fest, ob beim Wiederherstellen gescrollt wird (Standard: nein). - Titel:
ignoreTitle:trueimhx-swapoder globalhtmx.config.ignoreTitle, überall dort, wo die Antwort aus einer ganzen Seite geschnitten wird. - Sprache:
hx-boost="false"für Sprachwechsel. - Ansagen: eine Live-Region, die von Anfang an im Markup steht und die htmx nur befüllt.
- Ladezustand: htmx setzt die Klasse
htmx-requestund blendet damit Elemente mithtmx-indicatorein. Für assistive Technik ist das unsichtbar.aria-busysetzt htmx nicht. Im Quellcode von 2.0.11 kommt die Zeichenfolgeariakein einziges Mal vor.
Die htmx-Dokumentation erwähnt Barrierefreiheit im Zusammenhang mit Progressive Enhancement und gibt allgemeine Tipps wie semantisches HTML und sichtbaren Fokus. Zu Live-Regionen schweigt sie. Zur Fokusführung steht nur ein Satz in der hx-swap-Referenz: htmx erhält den Fokus bei Eingabefeldern mit id. Das ist kein Vorwurf. htmx ist bewusst klein, und diese Entscheidungen gehören in deine Anwendung. Du musst sie nur treffen.
Unpoly und die Fassung ohne Bibliothek
Unpoly hat in dieser Messung fast alles richtig gemacht, ohne dass ich eine Zeile dafür schreiben musste: Fokus auf den Tab, Fokus in der Liste statt auf <body> (wenn auch am Listenanfang), Overlay mit Fokusfalle und Rückgabe, Fokus auf <main> nach Seitenwechseln, korrekte Sprache. aria-busy setzt allerdings auch Unpoly nicht, der Ladezustand läuft über CSS-Klassen. Das ist der eigentliche Gegenwert für die knapp dreieinhalbfache Dateigröße aus Teil 1.
Die Fassung ohne Bibliothek hat die Probleme gar nicht erst, weil jeder Schritt ein echter Seitenwechsel ist und der Dialog nativ. Sie lädt bei jedem Schritt eine ganze Seite, ist aber die einzige, bei der ich nichts nachbessern müsste.
BFSG: warum das nicht nur Kür ist
Seit dem 28. Juni 2025 gilt in Deutschland das Barrierefreiheitsstärkungsgesetz (öffnet in neuem Tab) (BFSG). Es erfasst unter anderem Dienstleistungen im elektronischen Geschäftsverkehr, die für Verbraucher erbracht werden, also im Kern Onlineshops und Buchungsstrecken. Kleinstunternehmen, die Dienstleistungen anbieten, sind ausgenommen: weniger als zehn Beschäftigte und höchstens 2 Millionen Euro Jahresumsatz oder Jahresbilanzsumme. Für bestimmte Konstellationen laufen Übergangsfristen bis Mitte 2030.
WCAG nennt das Gesetz selbst nicht. Es verweist über eine Verordnung auf Anforderungen und über harmonisierte Normen auf eine Vermutung der Konformität. In der Praxis ist das die europäische Norm EN 301 549, deren im EU-Amtsblatt geführte Fassung auf WCAG 2.1 aufbaut. Eine neue Fassung mit WCAG 2.2 hat ETSI im September 2026 veröffentlicht, im Amtsblatt steht sie noch nicht.
Das ist keine Rechtsberatung. Aber wenn deine htmx-Anwendung Verbrauchern etwas verkauft, sind ein verlorener Fokus, ein falscher Seitentitel und eine falsche Sprachauszeichnung keine Schönheitsfehler mehr.
Was die Reihe gezeigt hat
Drei Teile, eine Demo, eine Erkenntnis in drei Richtungen:
- Einordnung (Teil 1): htmx ist klein, frei und schnell eingebaut. Seine Stärke ist, dass es wenig vorschreibt.
- Sichtbarkeit (Teil 2): Solange jeder Zustand eine echte URL hat, stand in meiner Messung alles in der ersten Antwort. Was nur per JavaScript nachkommt, sehen die meisten KI-Crawler nicht.
- Barrierefreiheit (dieser Teil): Genau weil htmx wenig vorschreibt, bleiben Fokus, Titel, Sprache und Ansagen bei dir. Unpoly nimmt dir davon viel ab, die Plattform mit echten Seitenwechseln und
<dialog>noch mehr.
Wenn ich eine Regel aus der ganzen Reihe mitnehme, dann diese: Bau zuerst die Fassung ohne JavaScript, und setz htmx nur dort drauf, wo es die Bedienung wirklich verbessert. Dann stimmen Sichtbarkeit und Grundgerüst von selbst, und du weißt genau, an welchen Stellen du dich um Fokus und Ansagen kümmern musst. Alle Teile im Überblick findest du in der htmx-Reihe. Die Barrierefreiheit der Seitenstruktur selbst behandelt der Artikel über HTML-Landmarks.