Schritt-Validierung und Fortschrittsanzeige
Mehrstufige Formulare erhöhen die Conversion, wenn sie UX-technisch sauber umgesetzt sind: klare Fortschrittsanzeige, Validierung pro Schritt statt erst beim Submit, und zuverlässige Rückwärtsnavigation ohne Datenverlust. Mit Alpine.js lässt sich dieses Pattern ohne externe Bibliothek vollständig realisieren.
Inhaltsverzeichnis
- 1. Warum Multi-Step statt einem langen Formular?
- 2. Datenstruktur: Schritte, Felder und Fehlerobjekte
- 3. Navigation: Vorwärts, Rückwärts und Fortschritt
- 4. Validierung: Schritt-für-Schritt statt Submit-Validierung
- 5. Fehleranzeige: Inline-Fehler pro Feld
- 6. Fortschrittsanzeige: Visuelles Feedback mit Tailwind
- 7. Submit-Handling und Ladezustand
- 8. Zugänglichkeit: ARIA-Attribute und Keyboard-Navigation
- 9. Vergleich: Alpine.js Wizard vs. VeeValidate vs. Formik
- 10. Zusammenfassung
- 11. FAQ
1. Warum Multi-Step statt einem langen Formular?
Lange Formulare auf einer einzigen Seite haben eine höhere Abbruchrate als mehrstufige Formulare, die denselben Inhalt auf logische Schritte aufteilen. Das liegt nicht allein an der Optik: Die kognitive Last sinkt, wenn Nutzer nur eine Gruppe zusammengehöriger Felder gleichzeitig sehen. Ein Checkout-Formular mit persönlichen Daten, Versandadresse, Zahlungsmethode und Bestellbestätigung ist inhaltlich in vier klar trennbare Blöcke aufteilbar – und genau das ist die natürliche Basis für einen Wizard.
In Magento 2 mit Hyvä ist das Checkout-Formular bereits ein mehrstufiger Prozess, aber im eigenen Code-Module oder im Theme entstehen häufig Anforderungen für Custom Wizards: Produktkonfiguratoren, Angebotsanfragen mit mehreren Schritten, Kundendaten-Erfassungsformulare nach der Registrierung, oder onboarding-Flows für B2B-Portale. Alpine.js ist für diese Anwendungsfälle ideal: kein Build-Step, kein Framework-Overhead, direkt in Phtml-Templates verwendbar. Die gesamte Wizard-Logik passt in eine einzige x-data-Funktion.
2. Datenstruktur: Schritte, Felder und Fehlerobjekte
Die Datenstruktur eines Alpine.js Wizards besteht aus wenigen reaktiven Eigenschaften. currentStep ist ein Integer (0-basiert), der den aktuell angezeigten Schritt speichert. totalSteps ist die Gesamtanzahl der Schritte, die vom Template abgelesen oder als Konstante definiert wird. formData ist ein flaches Objekt, das alle Formularfelder aller Schritte enthält – die Felder aller Schritte werden in einem einzigen Objekt gehalten, damit Daten beim Wechsel zwischen Schritten erhalten bleiben. errors ist ein paralleles Objekt mit denselben Keys, das pro Feld die Fehlermeldung oder null enthält.
Die Aufteilung, welche Felder zu welchem Schritt gehören, wird nicht in der Datenstruktur abgebildet, sondern im Template über x-show="currentStep === 0" auf den Schritt-Containern. Diese Trennung hält die JavaScript-Logik einfach und die Schritt-Konfiguration im HTML, wo sie visuell nachvollziehbar ist. Ein Array von Schritt-Definitionen – mit Namen, Feldern und Validierungsregeln – macht die Komponente konfigurierbarer, aber komplexer. Für die meisten Anwendungsfälle ist die einfachere Variante vorzuziehen.
// Multi-step wizard data structure with validation
function formWizard() {
return {
currentStep: 0,
totalSteps: 3,
isSubmitting: false,
isSuccess: false,
formData: {
// Step 1: Personal data
firstName: '',
lastName: '',
email: '',
// Step 2: Shipping address
street: '',
city: '',
zip: '',
country: 'DE',
// Step 3: Options
newsletter: false,
message: ''
},
errors: {
firstName: null,
lastName: null,
email: null,
street: null,
city: null,
zip: null,
country: null,
message: null
},
// Validation rules per step: returns array of field names for that step
stepFields: {
0: ['firstName', 'lastName', 'email'],
1: ['street', 'city', 'zip'],
2: [] // Optional step — no required fields
}
};
}
3. Navigation: Vorwärts, Rückwärts und Fortschritt
Die Vorwärtsnavigation im Wizard läuft immer über eine nextStep()-Methode, die vor dem Schrittwechsel eine Validierung des aktuellen Schritts durchführt. Nur wenn keine Fehler vorliegen, wird currentStep inkrementiert. Diese Kopplung ist der entscheidende UX-Vorteil des Wizard-Patterns: Nutzer können nur dann vorwärtsgehen, wenn der aktuelle Schritt gültig ist. Ein direkter Sprung zu späteren Schritten – etwa durch Klick auf den Fortschrittsbalken – ist nur sinnvoll, wenn der Sprung zu einem bereits validierten Schritt geht.
Die Rückwärtsnavigation in prevStep() ist deutlich simpler: Sie dekrementiert currentStep ohne jede Validierung. Das ist absichtlich, weil Nutzer jederzeit zurückgehen können müssen, ohne Daten zu verlieren. Ein häufiger Fehler: Beim Zurückgehen die Formulardaten des aktuellen Schritts zu löschen. Das ist aus UX-Sicht falsch – Nutzer erwarten, dass ihre eingegebenen Daten erhalten bleiben. Weil formData als ein gemeinsames Objekt über alle Schritte gehalten wird, passiert das bei diesem Pattern automatisch richtig.
4. Validierung: Schritt-für-Schritt statt Submit-Validierung
Die Validierungslogik wird in einer Methode validateStep(stepIndex) zentralisiert. Diese Methode liest die Feld-Liste für den gegebenen Schritt aus stepFields, durchläuft jeden Feldnamen und führt die entsprechende Validierungsregel aus. Fehler werden in this.errors[fieldName] geschrieben, bei Erfolg wird es auf null gesetzt. Die Methode gibt true zurück wenn keine Fehler gefunden wurden. Diese Rückgabe nutzt nextStep(), um den Schrittwechsel zu steuern.
Validierungsregeln können als einfache Funktionen definiert werden: required: v => v.trim() !== '', email: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), zip: v => /^\d{5}$/.test(v). Die Fehlermeldungen sollten spezifisch sein und dem Nutzer klar sagen, was zu tun ist – nicht nur was falsch ist. "Bitte geben Sie eine gültige E-Mail-Adresse ein" ist besser als "Ungültige E-Mail". Real-Time-Validierung bei @input oder @blur verbessert die UX zusätzlich, indem Fehler verschwinden sobald das Feld korrekt ausgefüllt wird.
// Step validation and navigation methods
{
validateStep(step) {
const fields = this.stepFields[step] || [];
let valid = true;
// Reset errors for this step's fields
fields.forEach(f => this.errors[f] = null);
for (const field of fields) {
const value = this.formData[field];
if (field === 'email') {
if (!value.trim()) {
this.errors.email = 'Bitte geben Sie Ihre E-Mail-Adresse ein.';
valid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
this.errors.email = 'Bitte geben Sie eine gültige E-Mail-Adresse ein.';
valid = false;
}
} else if (field === 'zip') {
if (!/^\d{4,5}$/.test(value)) {
this.errors.zip = 'Bitte geben Sie eine gültige Postleitzahl ein.';
valid = false;
}
} else {
if (!value?.toString().trim()) {
this.errors[field] = 'Dieses Feld ist erforderlich.';
valid = false;
}
}
}
return valid;
},
nextStep() {
if (this.validateStep(this.currentStep)) {
this.currentStep = Math.min(this.currentStep + 1, this.totalSteps - 1);
this.$nextTick(() => this.$el.scrollIntoView({ behavior: 'smooth' }));
}
},
prevStep() {
this.currentStep = Math.max(this.currentStep - 1, 0);
this.$nextTick(() => this.$el.scrollIntoView({ behavior: 'smooth' }));
}
}
5. Fehleranzeige: Inline-Fehler pro Feld
Inline-Fehler direkt unterhalb des betroffenen Feldes sind die nutzerfreundlichste Form der Fehleranzeige. Mit Alpine.js lassen sich diese über x-show und x-text deklarativ im Template steuern: <p x-show="errors.email" x-text="errors.email" class="text-red-500 text-sm mt-1"></p>. Das Element ist nur sichtbar wenn ein Fehler existiert, und zeigt automatisch die aktuelle Fehlermeldung an. Das Input-Feld selbst bekommt dynamisch einen Fehler-Rahmenstil: :class="{'border-red-500': errors.email, 'border-slate-300': !errors.email}".
Ein wichtiges UX-Detail: Fehler sollten erst nach dem ersten Versuch, weiterzugehen, angezeigt werden – nicht bereits beim Laden des Formulars. Das wird durch den Zustand des errors-Objekts automatisch sichergestellt: Alle Felder starten mit null, und Fehler werden nur durch validateStep() gesetzt, das erst bei nextStep() aufgerufen wird. Real-Time-Fehlerbehebung – also das Verschwinden eines Fehlers sobald das Feld korrekt ausgefüllt wird – lässt sich über @input="if (errors.email) validateField('email')" realisieren.
6. Fortschrittsanzeige: Visuelles Feedback mit Tailwind
Eine klare Fortschrittsanzeige ist eines der wichtigsten UX-Elemente eines Wizards. Sie zeigt dem Nutzer, wo er sich im Prozess befindet und wie viele Schritte noch folgen. Die einfachste Form ist ein Fortschrittsbalken: <div class="h-2 bg-teal-600 transition-all duration-300" :style="`width: ${(currentStep / (totalSteps - 1)) * 100}%`"></div>. Dieser Balken füllt sich mit jedem Schritt und gibt sofort visuelles Feedback.
Für mehrstufige Fortschrittsanzeigen mit Schritt-Labels eignet sich eine Punkte-Reihe mit Verbindungslinien. Jeder Punkt repräsentiert einen Schritt: grün für abgeschlossene Schritte, aktiv-markiert für den aktuellen, grau für zukünftige. In Alpine.js lässt sich das über x-for auf einem Array von Schritt-Objekten mit :class-Bindungen umsetzen. Das x-for-Pattern ist hier ideal, weil die Anzahl der Schritte im Template definiert wird und die Klassen deklarativ von currentStep abhängen.
7. Submit-Handling und Ladezustand
Der Submit-Schritt im Wizard ist der letzte Schritt und löst die tatsächliche Datenübertragung aus. Die Methode submit() setzt zunächst isSubmitting = true, um den Submit-Button zu deaktivieren und einen Ladeindikator anzuzeigen. Dann führt sie eine abschließende Validierung aller Schritte durch (nicht nur des letzten), um sicherzustellen, dass keine Felder leer sind. Danach wird der API-Aufruf mit fetch durchgeführt. Bei Erfolg wird isSuccess = true gesetzt und eine Erfolgsmeldung angezeigt. Bei Fehler wird eine allgemeine Fehlermeldung in submitError gespeichert.
Der isSubmitting-Zustand ist wichtig: Er verhindert Doppel-Submits durch schnelle Doppelklicks und gibt dem Nutzer visuelles Feedback, dass etwas passiert. Der Button bekommt :disabled="isSubmitting" und x-text="isSubmitting ? 'Wird gesendet…' : 'Absenden'". In Hyvä-Projekten kann der Submit-Endpunkt ein Magento-REST-Endpunkt sein oder ein Custom Controller, der über die Magento-API aufgerufen wird. Das Formular-Pattern bleibt unabhängig vom Backend-Endpunkt.
// Submit method with loading state and error handling
{
submitError: null,
async submit() {
// Final validation of all steps
let allValid = true;
for (let i = 0; i < this.totalSteps; i++) {
if (!this.validateStep(i)) allValid = false;
}
if (!allValid) {
this.submitError = 'Bitte prüfen Sie die markierten Felder.';
return;
}
this.isSubmitting = true;
this.submitError = null;
try {
const response = await fetch('/api/kontakt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.formData)
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.message || 'Fehler beim Senden');
}
this.isSuccess = true;
this.$dispatch('wizard-complete', { formData: this.formData });
} catch (e) {
this.submitError = e.message || 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.';
} finally {
this.isSubmitting = false;
}
}
}
8. Zugänglichkeit: ARIA-Attribute und Keyboard-Navigation
Ein zugänglicher Form Wizard benötigt mehrere ARIA-Anpassungen. Der Fortschrittsbereich bekommt role="group" mit aria-label="Formular-Fortschritt". Aktive Schritt-Indikatoren erhalten aria-current="step". Jeder Schritt-Container bekommt role="region" mit einem eindeutigen aria-labelledby-Attribut, das auf die Schritt-Überschrift zeigt. Diese Attribute helfen Screenreader-Nutzern, die Struktur des Wizards zu verstehen und den aktuellen Schritt zu identifizieren.
Die Keyboard-Navigation im Wizard sollte sicherstellen, dass beim Schrittwechsel der Fokus auf das erste Feld des neuen Schritts gesetzt wird. Das verhindert, dass der Fokus auf dem "Weiter"-Button verbleibt während ein neuer Schritt angezeigt wird. Mit Alpine.js: this.$nextTick(() => { const firstInput = this.$el.querySelector(`[data-step="${this.currentStep}"] input`); if (firstInput) firstInput.focus(); }). Fehlermeldungen werden mit role="alert" ausgezeichnet, damit Screenreader sie sofort ankündigen wenn sie erscheinen, ohne dass Nutzer aktiv navigieren müssen.
9. Vergleich: Alpine.js Wizard vs. VeeValidate vs. Formik
| Kriterium | Alpine.js (nativ) | VeeValidate (Vue) | Formik (React) |
|---|---|---|---|
| Bundle-Größe | 0 KB extra | ~32 KB gzip | ~12 KB gzip |
| Hyvä-kompatibel | Vollständig | Nein (Vue nötig) | Nein (React nötig) |
| Schema-Validierung | Selbst implementieren | Yup/Zod integriert | Yup/Zod integriert |
| Lernkurve | Niedrig | Mittel | Mittel–Hoch |
| Debugging | Direkter Zugriff | Vue DevTools | React DevTools |
Die Tabelle macht klar: Für Hyvä-Projekte gibt es keine echte Alternative zum nativen Alpine.js-Ansatz. VeeValidate und Formik setzen Vue bzw. React als Framework voraus, was in Hyvä nicht vorhanden ist. Der Nachteil des nativen Alpine.js-Ansatzes – das Selbst-Implementieren der Validierungsregeln – ist in der Praxis weniger schwerwiegend als es klingt: Die meisten Formulare benötigen nur eine Handvoll Regeln (required, email, zip, minLength), und diese lassen sich in weniger als zwanzig Zeilen implementieren.
Mironsoft
Alpine.js-Formulare, Hyvä Themes und Magento 2 Custom Modules
Custom Form Wizard für Magento 2 mit Hyvä?
Wir implementieren Multi-Step Formulare für Anfrage-Flows, Produktkonfiguratoren und Onboarding-Prozesse – vollständig in Alpine.js, CSP-konform, zugänglich und mit sauberer Backend-Integration.
Wizard-Entwicklung
Multi-Step Formulare mit Validierung, Fortschrittsanzeige und Submit-Handling
Backend-Integration
REST-Endpunkte, Magento-Controller oder externe APIs als Ziel des Formulars
Zugänglichkeit
WCAG-konforme ARIA-Attribute, Keyboard-Navigation und Screenreader-Tests
10. Zusammenfassung
Ein Multi-Step Form Wizard mit Alpine.js ist ein klares, wartbares Pattern ohne externe Abhängigkeiten. Die Datenstruktur ist einfach: ein flaches formData-Objekt für alle Schritte, ein paralleles errors-Objekt und ein Integer für den aktuellen Schritt. Validierung läuft pro Schritt in validateStep(), Navigation in nextStep() und prevStep(). Der Submit-Handler setzt isSubmitting als Schutz gegen Doppel-Submits und führt eine abschließende Gesamtvalidierung durch.
Das Pattern skaliert gut: Mit drei bis sieben Schritten bleibt die Komponente überschaubar. Für sehr viele Schritte oder komplexe bedingte Logik (Schritt C nur wenn Schritt B bestimmte Werte hat) empfiehlt sich eine Array-basierte Schritt-Konfiguration. Zugänglichkeit ist kein Nachgedanke: ARIA-Attribute, Fokus-Management beim Schrittwechsel und role="alert" auf Fehlermeldungen sollten von Anfang an Teil der Implementierung sein.
Alpine.js Form Wizard — Das Wichtigste auf einen Blick
Datenstruktur
formData flach für alle Schritte, paralleles errors-Objekt. Schritt-Zuordnung im Template über x-show, nicht im JS-Objekt.
Validierung
validateStep(step) gibt true/false zurück. nextStep() blockiert bei Fehler. Fehler in errors[field], null bei Erfolg.
Submit-Schutz
isSubmitting-Flag verhindert Doppel-Submits. Abschließende Gesamtvalidierung vor dem API-Aufruf. try/catch/finally für Ladezustand.
Zugänglichkeit
aria-current="step" auf aktivem Schritt, role="alert" auf Fehlermeldungen, Fokus-Sprung zu erstem Feld beim Schrittwechsel.