Barrierefreie Formulare bauen, die wirklich funktionieren
Formulare für alle: korrekte ARIA-Labels, Fehlerbehandlung, Tastaturnavigation und Screenreader-Support — jenseits von Checkbox-Compliance.

Formulare sind der Ort, an dem Nutzer dir ihre Daten anvertrauen – und genau dort tun Barrierefreiheitsfehler am meisten weh. Ein Screenreader-Nutzer, der nicht versteht, was ein Feld erwartet, ein Tastaturnutzer, der den Absende-Button nicht erreicht, ein Nutzer mit Sehbehinderung, der die Fehlermeldung nicht sehen kann – das sind keine Randfälle. Das sind Menschen, die dir Geld geben, sich für deinen Dienst anmelden oder kritische Informationen übermitteln wollen.
Die meisten "barrierefreien" Formulare bestehen automatisierte Prüfungen, scheitern aber an echten Nutzern. Der Checkbox-Compliance-Ansatz – "jedes Input hat ein Label" – verfehlt das Erlebnis. Wirklich barrierefreie Formulare kommunizieren klar, behandeln Fehler elegant und funktionieren mit jeder Eingabemethode.
Labels, die Absicht vermitteln
Die erste Regel barrierefreier Formulare: Jedes Input muss ein programmatisch zugeordnetes Label haben. Aber die Zuordnung allein reicht nicht – das Label muss vermitteln, was das Feld erwartet.
<!-- ❌ Label exists but doesn't help -->
<label for="field1">Field 1</label>
<input id="field1" type="text" />
<!-- ❌ Placeholder is not a label — disappears on input -->
<input type="email" placeholder="Enter your email" />
<!-- ✅ Clear label with format hint -->
<div class="field-group">
<label for="phone">Phone number</label>
<input
id="phone"
type="tel"
aria-describedby="phone-hint"
autocomplete="tel"
/>
<span id="phone-hint" class="hint">
Format: (555) 123-4567
</span>
</div>
<!-- ✅ Required fields clearly indicated -->
<div class="field-group">
<label for="email">
Email address
<span aria-hidden="true" class="required">*</span>
</label>
<input
id="email"
type="email"
required
aria-required="true"
autocomplete="email"
aria-describedby="email-hint"
/>
<span id="email-hint" class="hint">
We'll send your confirmation here
</span>
</div>Das Attribut aria-describedby verknüpft ergänzenden Text mit dem Input. Screenreader geben ihn nach dem Label aus und liefern den Nutzern den vollständigen Kontext: "Email address, required. Edit text. We'll send your confirmation here."
Fehlerbehandlung, die anleitet
Fehlermeldungen müssen drei Zielgruppen gleichzeitig erreichen: sehende Nutzer, die visuell scannen, Screenreader-Nutzer, die programmatisch navigieren, und Tastaturnutzer, die mit Tab durch das Formular gehen.
<!-- ✅ Accessible error handling pattern -->
<form novalidate aria-label="Registration form">
<!-- Error summary at top of form -->
<div
id="error-summary"
role="alert"
aria-live="assertive"
class="error-summary"
hidden
>
<h2>There are 2 problems with your submission</h2>
<ul>
<li><a href="#email">Enter a valid email address</a></li>
<li><a href="#password">Password must be at least 8 characters</a></li>
</ul>
</div>
<div class="field-group">
<label for="email">Email address</label>
<input
id="email"
type="email"
required
aria-required="true"
aria-invalid="true"
aria-describedby="email-error"
autocomplete="email"
/>
<span id="email-error" class="error" role="alert">
Enter a valid email address
</span>
</div>
<div class="field-group">
<label for="password">Password</label>
<input
id="password"
type="password"
required
aria-required="true"
aria-invalid="true"
aria-describedby="password-error password-requirements"
autocomplete="new-password"
/>
<span id="password-error" class="error" role="alert">
Password must be at least 8 characters
</span>
<span id="password-requirements" class="hint">
At least 8 characters with one uppercase letter and one number
</span>
</div>
</form>// Form validation with accessible error handling
class AccessibleForm {
private form: HTMLFormElement;
private errorSummary: HTMLElement;
constructor(form: HTMLFormElement) {
this.form = form;
this.errorSummary = form.querySelector('#error-summary')!;
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
private handleSubmit(e: Event): void {
e.preventDefault();
const errors = this.validate();
if (errors.length > 0) {
this.showErrors(errors);
} else {
this.clearErrors();
this.submitForm();
}
}
private showErrors(
errors: Array<{ fieldId: string; message: string }>
): void {
// Show error summary
this.errorSummary.hidden = false;
const list = this.errorSummary.querySelector('ul')!;
list.innerHTML = '';
for (const error of errors) {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `#${error.fieldId}`;
link.textContent = error.message;
li.appendChild(link);
list.appendChild(li);
// Mark individual fields
const field = document.getElementById(error.fieldId);
if (field) {
field.setAttribute('aria-invalid', 'true');
const errorEl = document.getElementById(`${error.fieldId}-error`);
if (errorEl) {
errorEl.textContent = error.message;
errorEl.hidden = false;
}
}
}
// Move focus to error summary so screen readers announce it
this.errorSummary.focus();
}
private clearErrors(): void {
this.errorSummary.hidden = true;
this.form.querySelectorAll('[aria-invalid]').forEach((el) => {
el.removeAttribute('aria-invalid');
});
}
}Tastaturnavigation, die fließt
Jede Interaktion in deinem Formular muss ohne Maus funktionieren. Das bedeutet: logische Tab-Reihenfolge, sichtbare Fokusindikatoren und tastaturbedienbare benutzerdefinierte Steuerelemente.
/* ✅ Visible focus indicators — never remove outline without replacement */
/* ❌ Don't do this: *:focus { outline: none; } */
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
/* For browsers that support it, only show focus ring on keyboard nav */
input:focus:not(:focus-visible) {
outline: none;
}
input:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
/* Error state styling */
input[aria-invalid="true"] {
border-color: #dc2626;
border-width: 2px;
}
input[aria-invalid="true"]:focus {
outline-color: #dc2626;
}
/* Skip link — first focusable element on the page */
.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 0.5rem 1rem;
background: #1e293b;
color: white;
z-index: 100;
}
.skip-link:focus {
top: 0;
}// ✅ Custom dropdown that works with keyboard
class AccessibleSelect {
private button: HTMLButtonElement;
private listbox: HTMLUListElement;
private options: HTMLLIElement[];
private activeIndex: number = -1;
handleKeyDown(event: KeyboardEvent): void {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.activeIndex = Math.min(
this.activeIndex + 1,
this.options.length - 1
);
this.updateActiveDescendant();
break;
case 'ArrowUp':
event.preventDefault();
this.activeIndex = Math.max(this.activeIndex - 1, 0);
this.updateActiveDescendant();
break;
case 'Enter':
case ' ':
event.preventDefault();
if (this.activeIndex >= 0) {
this.selectOption(this.activeIndex);
}
break;
case 'Escape':
this.closeDropdown();
this.button.focus();
break;
case 'Home':
event.preventDefault();
this.activeIndex = 0;
this.updateActiveDescendant();
break;
case 'End':
event.preventDefault();
this.activeIndex = this.options.length - 1;
this.updateActiveDescendant();
break;
}
}
private updateActiveDescendant(): void {
const activeOption = this.options[this.activeIndex];
this.listbox.setAttribute(
'aria-activedescendant',
activeOption.id
);
activeOption.scrollIntoView({ block: 'nearest' });
}
}Mehrstufige Formulare und Fortschritt
Komplexe Formulare, die auf mehrere Schritte aufgeteilt sind, brauchen klare Fortschrittsanzeigen und die Möglichkeit, zwischen den Schritten zu navigieren.
<!-- ✅ Accessible multi-step form -->
<div role="group" aria-label="Checkout form">
<!-- Step indicator -->
<nav aria-label="Checkout progress">
<ol class="steps">
<li aria-current="step">
<span class="step-number">1</span>
<span>Shipping</span>
</li>
<li>
<span class="step-number">2</span>
<span>Payment</span>
</li>
<li>
<span class="step-number">3</span>
<span>Review</span>
</li>
</ol>
</nav>
<!-- Current step -->
<section aria-label="Step 1: Shipping information">
<h2>Shipping information</h2>
<!-- Live region announces step changes -->
<div
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
Step 1 of 3: Shipping information
</div>
<!-- Form fields for this step -->
<div class="field-group">
<label for="address">Street address</label>
<input
id="address"
type="text"
required
autocomplete="street-address"
/>
</div>
<div class="button-group">
<button type="button" disabled>Previous</button>
<button type="button" onclick="nextStep()">
Continue to payment
</button>
</div>
</section>
</div>Barrierefreiheit wirklich testen
Automatisierte Tools finden etwa 30 % der Barrierefreiheitsprobleme. Der Rest erfordert manuelle Tests mit echter assistiver Technologie.
## Accessibility testing checklist for forms
### Automated (run first)
- [ ] axe-core or Lighthouse audit: zero violations
- [ ] HTML validator: no markup errors
- [ ] All inputs have associated labels
- [ ] All images have alt text
### Keyboard testing (5 minutes)
- [ ] Tab through entire form — logical order?
- [ ] Every interactive element reachable via Tab
- [ ] Focus indicator visible on every element
- [ ] Custom controls work with Enter/Space
- [ ] Dropdown menus work with arrow keys
- [ ] Escape closes dropdowns/modals
- [ ] No keyboard traps (can always Tab out)
### Screen reader testing (15 minutes)
- [ ] NVDA or VoiceOver: navigate form by tab
- [ ] Each field announces: label, type, required state
- [ ] Error messages announced when they appear
- [ ] Error summary focused and read on submit
- [ ] Format hints read after label
- [ ] Multi-step progress communicated
### Visual testing
- [ ] Errors visible without relying on color alone
- [ ] Text meets 4.5:1 contrast ratio
- [ ] Form usable at 200% zoom
- [ ] Form works in high contrast modeDie wichtigsten Erkenntnisse
Jedes Formularfeld muss ein programmatisch zugeordnetes Label über die Attribute for/id haben – Placeholder sind keine Labels, weil sie verschwinden, sobald Nutzer zu tippen beginnen, und Screenreader sie möglicherweise nicht konsistent ansagen. Die Fehlerbehandlung braucht drei Ebenen: eine Fehlerübersicht am Anfang des Formulars, die beim Absenden den Fokus erhält, einzelne Fehlermeldungen, die über aria-describedby mit den Feldern verknüpft sind, und aria-invalid-Attribute auf problematischen Feldern, damit assistive Technologie den Zustand kommuniziert. Entferne niemals Fokus-Umrandungen, ohne einen sichtbaren Ersatz anzubieten – nutze :focus-visible, um Fokusringe nur bei Tastaturnavigation anzuzeigen; so bleibt das Formular für Mausnutzer aufgeräumt und für Tastaturnutzer bedienbar. Teste mit echter assistiver Technologie, nicht nur mit automatisierten Scannern – durchlaufe jedes Formular mit der Tastatur, navigiere es mit einem Screenreader und zoome auf 200 %, denn automatisierte Tools wie Lighthouse erkennen etwa 30 % der Barrierefreiheitsprobleme, während manuelle Tests die Erlebnislücken aufdecken, die am wichtigsten sind.


