Komponenten · Formulare
Toggle-Switches & Custom-Checkboxen
Eigene Schalter und Checkboxen baust du barrierefrei, indem du das native <input> behältst und es mit appearance: none direkt gestaltest – statt es zu verstecken und durch ein <div> zu ersetzen. Beim Toggle-Switch kommt genau eine von zwei Auszeichnungen dazu: role="switch" für ein Formularfeld, das einen Zustand speichert, oder aria-pressed für einen Button, der sofort etwas tut.
Schick gestaltete Bedienelemente sind eine der häufigsten Barrierefallen überhaupt – und der Grund ist fast immer derselbe: Das native Element wird ersetzt statt gestaltet. Dabei ist der Umweg seit Jahren unnötig. appearance: none gibt dir volle Kontrolle über das Aussehen, ohne dass du Fokus, Tastatur, Formularverhalten und Ansage neu erfinden musst. Die erste Regel von ARIA zeigt die Richtung; diese Seite zeigt die Umsetzung im Detail.
Das Wichtigste in Kürze
-
appearance: nonestatt Verstecken. Das native<input>bleibt sichtbar an Ort und Stelle und wird direkt gestaltet – kein Overlay, kein<div>-Nachbau. -
display: noneist ein Fehler,.visually-hiddenbestenfalls die zweite Wahl: Wer per Touch-Screenreader tastend sucht, findet ein 1×1 Pixel großes Feld nicht. - Schalter, zwei Wege:
role="switch"(+aria-checked) auf einem Formularfeld,aria-pressedauf einem Button mit Sofortwirkung. Nicht beides mischen. - Der Zustand darf nie nur farbig sein – Position, Text oder Icon müssen ihn ebenfalls zeigen (1.4.1).
- Kontrastmodus mitdenken: Im erzwungenen Farbmodus verschwinden Hintergründe und Schatten. Prüfe mit
@media (forced-colors: active)und Systemfarben. - 24 × 24 CSS-Pixel Zielgröße als Untergrenze (2.5.8) – das schmale 20-px-Kästchen aus dem Design ist zu klein, das verknüpfte Label rettet es.
- Das Label benennt die Funktion, nicht den Zustand. „Benachrichtigungen“ ist richtig, „Benachrichtigungen aktivieren“ kollidiert mit der Ansage „ein/aus“.
Warum der <div>-Nachbau scheitert
Ein <div> mit Klick-Handler sieht identisch aus und verliert dabei fünf Dinge auf einmal, die ein <input> gratis mitbringt:
| Was verloren geht | Folge |
|---|---|
| Fokussierbarkeit | Per Tastatur nicht erreichbar (2.1.1) |
| Rolle & Zustand | Screenreader sagt „Klickbar“, nicht „Kontrollkästchen, angehakt“ |
| Leertasten-Verhalten | Muss selbst nachgebaut werden, inklusive Scroll-Unterdrückung |
| Formularwert | Wird beim Absenden nicht mitgeschickt |
| Label-Verknüpfung | for/id greift nicht, der Klick aufs Label tut nichts |
Jedes dieser fünf Dinge lässt sich nachrüsten – aber jedes ist eine eigene Fehlerquelle, und zusammen sind sie ein <input>, das schlechter funktioniert als das echte. Wer role="checkbox", tabindex="0", aria-checked und einen Keydown-Handler schreibt, hat viel Arbeit investiert, um am Ende auf demselben Stand zu sein.
Custom-Checkboxen: drei Techniken, eine Empfehlung
Es gibt drei verbreitete Wege, eine Checkbox anders aussehen zu lassen. Sie sind nicht gleichwertig.
Weg 1: display: none am Input. Der Klassiker aus alten Tutorials und schlicht falsch. Ein per display: none oder visibility: hidden ausgeblendetes Feld ist aus der Tab-Reihenfolge und aus dem Accessibility-Tree verschwunden. Es existiert für Tastatur und Screenreader nicht mehr.
Weg 2: Input visuell verstecken, Ersatz danebenstellen. Das Input bekommt eine .visually-hidden-Klasse (absolut positioniert, 1 × 1 px, clip-path), und ein ::before am Label übernimmt die Optik. Das funktioniert – Fokus und Tastatur bleiben erhalten –, hat aber zwei Haken: Der Fokusring muss per input:focus-visible + label::before umgeleitet werden (vergisst man das, ist der Fokus unsichtbar), und Sara Soueidan weist auf ein Problem hin, das man am Schreibtisch nie bemerkt: Wer mit einem Touch-Screenreader tastend über den Bildschirm fährt, sucht das Bedienelement dort, wo es aussieht – und findet an dieser Stelle nichts, weil das echte Feld ein Pixel groß in einer Ecke sitzt. Ihre Fassung dieses Musters legt das transparente Input deshalb über die Grafik statt es wegzuschieben.
Weg 3: appearance: none direkt am Input. Der heute empfohlene Weg. Das Feld bleibt genau dort, wo es hingehört, in Originalgröße, und wird selbst gestaltet:
input[type="checkbox"] {
appearance: none;
/* Schriftgröße erben, damit alles in em skaliert */
font: inherit;
color: currentColor;
width: 1.15em;
height: 1.15em;
border: 0.15em solid currentColor;
border-radius: 0.15em;
display: grid;
place-content: center;
}
/* Der Haken – als Form, nicht als Zeichen */
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
transform: scale(0);
transition: transform 120ms ease-in-out;
background-color: CanvasText;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
input[type="checkbox"]:checked::before { transform: scale(1); }
input[type="checkbox"]:focus-visible {
outline: max(2px, 0.15em) solid var(--color-focus);
outline-offset: max(2px, 0.15em);
}
Drei Details lohnen den zweiten Blick. currentColor erbt die Textfarbe, sodass die Checkbox in jedem Kontext mitzieht – auch im Kontrastmodus. Das clip-path zeichnet den Haken als Form, nicht als Unicode-Zeichen: Das ist unabhängig von der Schriftart und skaliert sauber. Und max(2px, 0.15em) sorgt dafür, dass der Fokusring auch bei sehr kleiner Schrift nicht unter 2 px fällt.
Für Radiobuttons gilt dasselbe – nur mit border-radius: 50% und einem gefüllten Kreis statt Haken. Und die Gruppierung übernimmt weiterhin fieldset und legend.
Der schnellste Weg: accent-color
Wenn es nur um die Markenfarbe geht, brauchst du gar kein Custom-Styling:
:root { accent-color: var(--color-primary); }
Eine Zeile, und Checkboxen, Radios, Fortschrittsbalken und Range-Slider nehmen deine Farbe an. Der Browser wählt die Farbe des Hakens automatisch so, dass der Kontrast stimmt – ein seltener Fall, in dem Nichtstun die zugänglichere Lösung ist. Der Preis: Die Form bleibt die des Betriebssystems und sieht auf macOS anders aus als auf Windows.
Der blinde Fleck: erzwungener Farbmodus
Windows-Kontrastmodus (und forced-colors allgemein) ersetzt Farben durch eine kleine Systempalette und entfernt Hintergrundbilder, Schatten und oft auch Hintergrundfarben. Ein Schalter, dessen „an“-Zustand nur aus background: green besteht, sieht dort identisch aus wie „aus“. Das ist der häufigste Bug in ansonsten sauberen Komponenten.
@media (forced-colors: active) {
input[type="checkbox"] {
/* Rahmen und Haken folgen der Systemfarbe */
border-color: ButtonText;
}
input[type="checkbox"]::before {
background-color: Highlight;
}
input[type="checkbox"]:disabled {
border-color: GrayText;
}
}
CanvasText, ButtonText, Highlight und GrayText sind Systemfarben, die der Kontrastmodus nicht wegwirft. Wer currentColor und diese Schlüsselwörter nutzt, bekommt den Kontrastmodus fast geschenkt. Testen lässt sich das in den Chrome-DevTools unter Rendering → Emulate CSS media feature forced-colors.
Toggle-Switch: drei Wege, einer davon neu
Ein Schalter („an/aus“) ist kein eigenes HTML-Element – oder war es lange nicht. Heute gibt es drei legitime Umsetzungen.
Der native Weg (noch nicht überall). Safari kennt seit Version 17.4 ein switch-Attribut an der Checkbox:
<input type="checkbox" switch id="darkmode" name="darkmode" />
<label for="darkmode">Dunkles Design</label>
Browser, die es nicht kennen, ignorieren das Attribut und zeigen eine ganz normale Checkbox – die Seite bleibt also funktionsfähig. Das ist Progressive Enhancement im Lehrbuchsinn. Nur: Stand Juli 2026 ist Safari weiterhin der einzige Browser mit Unterstützung, und ein Schalter, der auf drei Vierteln der Geräte wie eine Checkbox aussieht, ist ein Designproblem. Für Steuerungen im eigenen Admin-Bereich schon heute brauchbar, für die öffentliche Seite noch nicht.
Checkbox mit role="switch" – wenn ein Wert gespeichert wird. Das ist der Weg für Formularfelder, die abgeschickt werden:
<input type="checkbox" role="switch" id="mails" name="mails" checked />
<label for="mails">Wöchentliche Zusammenfassung</label>
Wichtig: aria-checked schreibst du hier nicht selbst. Der Browser leitet es aus checked ab; ein handgesetztes aria-checked würde nur irgendwann auseinanderlaufen.
Button mit aria-pressed – wenn sofort etwas passiert. Kein Formularwert, kein Absenden, die Wirkung tritt beim Klick ein:
<button type="button" aria-pressed="false" id="ton">Ton stumm schalten</button>
const btn = document.getElementById('ton');
btn.addEventListener('click', () => {
const an = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!an));
});
Hier musst du den Zustand selbst pflegen – und zwar immer am selben Element, das auch den Fokus trägt.
Welcher Weg wann?
| Situation | Auszeichnung | Zustand kommt von |
|---|---|---|
| Einstellung im Formular, wird abgeschickt | <input type="checkbox" role="switch"> |
checked (automatisch) |
| Einstellung, die sofort wirkt, JS vorausgesetzt | <button role="switch"> |
aria-checked (selbst gesetzt) |
| Aktion mit Zwei-Zustands-Charakter („stumm“, „merken“) | <button aria-pressed> |
aria-pressed (selbst gesetzt) |
| Auswahl aus mehreren Optionen | normale Checkbox, kein Schalter | checked |
Die häufigste Verwechslung: aria-pressed an einem Element mit role="switch". Beides zusammen ist widersprüchlich – ein Switch nutzt aria-checked, ein Toggle-Button aria-pressed. Mehr zur Systematik dahinter auf der Seite Rollen, States & Properties.
Was Screenreader wirklich daraus machen
role="switch" ist gut spezifiziert, aber nicht überall gleich gut umgesetzt. Adrian Roselli hat die Kombinationen systematisch durchgetestet, und das Ergebnis ist ernüchternd genug, um die eigene Umsetzung danach auszurichten:
- VoiceOver auf macOS sagt sauber „ein“ / „aus“.
- NVDA mit Firefox meldete Schalter lange schlicht als Kontrollkästchen – „angehakt“ statt „ein“.
- VoiceOver auf iOS ignorierte die Switch-Rolle über Jahre komplett.
-
aria-checked="mixed"ist an einem Switch ungültig und wird von Browsern unterschiedlich falsch behandelt. Nicht verwenden.
Die praktische Konsequenz ist nicht „lass es sein“, sondern: Verlass dich nie allein auf die Rolle. Wenn der Unterschied zwischen „angehakt“ und „ein“ für das Verständnis zählt, gehört er zusätzlich in sichtbaren Text – ein „An“/„Aus“ neben dem Schalter oder ein Label, das ohne Zustandswissen funktioniert. Wie man so etwas selbst nachprüft, steht unter Mit NVDA testen und Mit VoiceOver testen.
Randnotiz – Zustand ansagen, nicht nur färben. Ein Schalter, der seinen Zustand nur über Farbe oder Position zeigt, ist für Screenreader stumm und für Farbfehlsichtige mehrdeutig.
role="switch"/aria-pressedliefern die Ansage „ein/aus“; ein zusätzliches Textlabel oder Icon macht den Zustand auch sichtbar unabhängig von der Farbe. Und der Schalter selbst braucht 3 : 1 Kontrast gegen seinen Hintergrund (1.4.11) – der beliebte hellgraue „Aus“-Zustand reißt diese Grenze regelmäßig.
Größe, Label und Tastatur
Zielgröße. Ein 20 × 20 px großes Kästchen unterschreitet die 24 × 24 CSS-Pixel aus 2.5.8. Die Ausnahme greift, wenn ein gleichwertiges Ziel danebenliegt – und genau das ist ein korrekt verknüpftes Label: Es ist Teil der Klickfläche. Ein Label mit padding und min-height: 24px löst das Problem elegant.
Label-Text. Das Label benennt, was geschaltet wird, nicht den Zustand. „Dunkles Design“ plus Ansage „ein“ ergibt einen klaren Satz. „Dunkles Design aktivieren“ plus Ansage „aus“ ergibt Ratespiele. Details unter Labels & Beschriftungen.
Tastatur. Checkbox und Switch-Checkbox schalten mit der Leertaste. Ein <button> reagiert zusätzlich auf Enter. Baust du einen Schalter aus einem <div> nach, musst du beides selbst abfangen – und event.preventDefault() bei der Leertaste nicht vergessen, sonst scrollt die Seite.
Häufige Fehler
-
<div>statt<input>/<button>– Fokus, Tastatur und Formularwert fehlen. -
Natives Input mit
display: noneversteckt – es ist gar nicht mehr fokussierbar. -
.visually-hiddenohne umgeleiteten Fokusring – der Fokus ist unsichtbar. - Zustand nur per Farbe ohne Text, Icon oder Position.
-
Kein
@media (forced-colors: active)– im Kontrastmodus sehen an und aus gleich aus. -
aria-pressedzusammen mitrole="switch"– widersprüchliche Auszeichnung. -
aria-checkedvon Hand an einer echten Checkbox – läuft mitcheckedauseinander. - Schalter kleiner als 24 × 24 px ohne klickbares Label als Ausgleich.
-
aria-checked="mixed"an einem Switch – ungültig.
Checkbox mit role="switch" oder Button mit aria-pressed? Der Komponenten-Baukasten zeigt beide Varianten samt der erwarteten Screenreader-Ausgabe – daran wird der Unterschied am schnellsten klar.
Häufige Fragen
Checkbox oder Switch – wann was?
Ein Switch ist ein Sonderfall der Checkbox für „an/aus“, der meist sofort wirkt (z. B. Dunkelmodus). Geht es um eine Auswahl, die erst beim Absenden zählt – etwa „AGB akzeptieren“ oder eine Mehrfachauswahl –, ist die normale Checkbox richtig. Als Faustregel: Klingt „ein/aus“ vorgelesen sinnvoller als „angehakt/nicht angehakt“, ist es ein Switch.
role="switch" oder aria-pressed?
role="switch" gehört auf ein Element, das einen Zustand speichert – der Zustand kommt dann über aria-checked bzw. bei einer Checkbox automatisch über checked. aria-pressed gehört auf einen Button, der eine Aktion umschaltet. Beides ist korrekt, aber nie gleichzeitig am selben Element.
Ist appearance: none sicher einsetzbar?
Ja. Die Kombination aus appearance: none und gestalteten Pseudo-Elementen wird seit 2017 von Chrome, Safari und Firefox unterstützt, seit 2020 auch von Edge. Wichtig ist nur, den Fokusring selbst zu setzen – appearance: none entfernt auch den vom Browser mitgelieferten.
Brauche ich noch die .visually-hidden-Variante?
Nur, wenn das Design eine Form verlangt, die sich am Input selbst nicht bauen lässt – etwa ein komplexes Inline-SVG mit mehreren Pfaden. Dann gilt Sara Soueidans Fassung: Das transparente Input liegt über der Grafik, nicht in einer Bildschirmecke.
Wie teste ich, ob mein Schalter im Kontrastmodus funktioniert?
In Chrome oder Edge über die DevTools: ⌘/Strg + Shift + P → „Show Rendering“ → Emulate CSS media feature forced-colors: active. Unter Windows geht es echt über Einstellungen → Barrierefreiheit → Kontrastdesigns.
Darf der Schalter beschriftet sein mit „An“ und „Aus“ direkt daneben?
Ja, und das ist sogar hilfreich – solange die beiden Wörter nicht das eigentliche Label ersetzen. Der Screenreader sagt den Zustand ohnehin an; der sichtbare Text hilft allen anderen, die ihn nur an Farbe und Position ablesen müssten.
Fazit
Schalter und eigene Checkboxen baust du am besten nicht nach: Behalte das native <input> oder <button> und gestalte es direkt mit appearance: none – dann bleiben Fokus, Tastatur, Formularwert und Ansage erhalten, ohne dass du sie nachrüstest. Beim Zustand entscheidet die Funktion über die Auszeichnung: role="switch" für gespeicherte Werte, aria-pressed für Sofortaktionen, nie beides. Und weil role="switch" je nach Screenreader unterschiedlich ankommt, gehört „ein/aus“ zusätzlich in etwas Sichtbares – Position, Icon oder Text, nur eben nicht Farbe allein. Das ist derselbe Gedanke wie in der ersten Regel von ARIA: So viel Plattform wie möglich, so wenig Nachbau wie nötig.
Quellen
- Switch Role Support (Adrian Roselli – systematische Tests, welche Screenreader-Browser-Kombination `role="switch"` korrekt ansagt)
- Inclusively Hiding & Styling Checkboxes and Radio Buttons (Sara Soueidan – warum `.sr-only` für Bedienelemente die falsche Technik ist)