Bereiche
Formulare und Formularzustände
Ganze Formulare als Daten beschrieben, eine Fehlerzusammenfassung, die zu jedem Feld führt, und Zustände für Erfolg, Fehler und gespeicherte Entwürfe. Gebaut, damit Menschen beenden, was sie beginnen.
Auf dieser Seite
Das passende Formular wählen#
Für Anfragen, Bewerbungen, Anmeldungen, Umfragen und alles nach dem Absenden gibt es fertige Sections in Bewerbungen und Anmeldungen, Anfrageformulare, Adresse, Zahlung und Einwilligung, Umfragen und Feedback und Formularzustände. Einzelne Felder wie Telefon, Uhrzeit oder Unterschrift findest du unter Spezielle Eingabefelder.
| Component | Varianten | Wofür |
|---|---|---|
| multi-step-form | rail, progress-top, review, accordion, live-summary | Längere Anfragen in Schritten, mit Fortschritt, Prüfseite oder laufender Zusammenfassung. |
| request-quote | brief, line-items, by-type, call-or-form | Angebotsanfragen als Briefing, mit Positionen oder nach Art des Auftrags. |
| booking-request | event-venue, flex-dates, packages, date-options | Anfragen für Termin, Tisch oder Raum, die jemand bestätigt. |
| callback-request | time-slots, now-or-later, inline-bar, topic-agent | Ein Rückruf im gewählten Zeitfenster oder sofort. |
| application-form | job, quick-apply, guided-letter, eligibility | Bewerbungen für Stellen und Programme mit Unterlagen. |
| registration | attendees, sessions, team, tickets | Anmeldungen für Events, Kurse und Vereine. |
| file-request | checklist, single-drop, portal, review-status | Unterlagen von Kunden einsammeln, mit klarer Checkliste. |
| consent-preferences | email-topics, unsubscribe, channel-matrix, privacy-requests | Einwilligungen und Kontaktwünsche, die man ändern kann. |
| address-form | international, shipping-billing, address-book, lookup | Adressen für Lieferung und Rechnung, für jedes Land gesetzt. |
| payment-form | methods, split-summary, direct-debit, installments | Karte, Lastschrift und Wallet in einem ruhigen Formular. |
| survey | scale, matrix, choice, open, branching | Umfragen mit einer Frage je Schritt oder alles auf einer Seite. |
| success-states | next-steps, receipt, submitted-summary, status-tracker | Was Menschen nach dem Absenden sehen. Nächste Schritte, Beleg, Zusammenfassung, Status. |
| error-recovery | summary-top, inline-live, submit-failed, session-expired | Fehler mit Ausweg, von der Zusammenfassung bis zur abgelaufenen Sitzung. |
| save-progress | autosave, resume-link, drafts, resume-banner | Automatisch speichern, Link zum Fortsetzen, Entwürfe und ein Banner zum Weitermachen. |
Felder als Daten#
Die Formular-Sections beschreiben ihre Felder als Daten, als Liste von FieldSpec. So passt ein ganzes Formular in JSON, im Studio genauso wie in deinem CMS. Prüfung, Fehlertexte und Zusammenfassung kommen mit.
import { MultiStepForm } from '@/components/ui/multi-step-form';
<MultiStepForm
variant="review"
title="Tisch für deine Gruppe anfragen"
steps={[
{
id: 'event',
title: 'Dein Abend',
fields: [
{ name: 'date', label: 'Datum', kind: 'date', required: true, half: true },
{ name: 'guests', label: 'Gäste', kind: 'number', required: true, half: true },
{ name: 'occasion', label: 'Anlass', kind: 'chips', options: [{ value: 'birthday', label: 'Geburtstag' }, { value: 'team', label: 'Teamessen' }] },
],
},
{
id: 'contact',
title: 'Kontakt',
fields: [
{ name: 'name', label: 'Name', required: true, autoComplete: 'name' },
{ name: 'email', label: 'E-Mail', kind: 'email', required: true, autoComplete: 'email' },
{ name: 'privacy', label: 'Ihr dürft meine Angaben für diese Anfrage nutzen', kind: 'consent', required: true },
],
},
]}
done={{ title: 'Anfrage ist da', text: 'Wir bestätigen deinen Tisch innerhalb eines Werktags.' }}
onSubmit={(values) => send(values)}
/>| Prop | Typ | Standard | Wofür |
|---|---|---|---|
| name | string | · | Schlüssel des Werts im Ergebnis. Auch die id, zu der die Fehlerzusammenfassung springt. |
| label | string | · | Das sichtbare Label, immer über dem Feld. |
| kind | 'text' | 'email' | 'tel' | 'number' | 'date' | 'textarea' | 'select' | 'choice' | 'chips' | 'consent' | 'text' | Die Art des Felds und seine eingebaute Prüfung. |
| required | boolean | · | Das Feld muss gefüllt sein. Freiwillige Felder tragen keine Markierung. |
| options | { value, label, description?, icon? }[] | · | Auswahl für select, choice und chips. |
| hint | string | · | Hilfe unter dem Label, für Screenreader mit dem Feld verbunden. |
| error | string | · | Deine eigene Meldung. Ohne sie sagt das Kit, was fehlt und wofür. |
| half | boolean | · | Halbe Breite ab Tablet, zwei halbe Felder teilen sich eine Zeile. |
| autoComplete | string | · | Das autocomplete-Token, damit Browser Namen und Adressen ausfüllen. |
Fehlerzusammenfassung#
Für eigene Formulare stellt form-flow die Teile bereit, die alle Formular-Sections teilen. checkFields prüft die Werte, ErrorSummary nennt jedes offene Feld mit einem Sprunglink, useFocusFirstError setzt nach jedem Versuch den Fokus. Die Texte übernehmen die Verantwortung und sagen, was fehlt und wofür.
import { useRef, useState } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { ErrorSummary, checkFields, useFlowTexts, useFocusFirstError, type FieldSpec, type FlowValues } from '@/components/ui/form-flow';
const FIELDS: FieldSpec[] = [
{ name: 'name', label: 'Name', required: true },
{ name: 'email', label: 'E-Mail', kind: 'email', required: true },
];
export function ContactForm() {
const t = useFlowTexts();
const [values, setValues] = useState<FlowValues>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const [attempt, setAttempt] = useState(0);
const form = useRef<HTMLFormElement>(null);
const summary = useRef<HTMLDivElement>(null);
useFocusFirstError(form, attempt, summary);
return (
<form
ref={form}
noValidate
onSubmit={(e) => {
e.preventDefault();
const found = checkFields(FIELDS, values, t);
setErrors(found);
if (Object.keys(found).length) setAttempt((n) => n + 1);
else send(values);
}}
className="grid gap-4"
>
<ErrorSummary ref={summary} errors={Object.entries(errors).map(([field, message]) => ({ field, message }))} />
{FIELDS.map((f) => (
<Input
key={f.name}
id={f.name}
label={f.label}
type={f.kind === 'email' ? 'email' : 'text'}
required={f.required}
error={errors[f.name]}
value={String(values[f.name] ?? '')}
onChange={(e) => setValues({ ...values, [f.name]: e.target.value })}
/>
))}
<Button type="submit">Anfrage senden</Button>
</form>
);
}Barrierefreiheit#
- Jedes Feld hat ein sichtbares Label, Hinweise und Fehler sind per
aria-describedbymit ihm verbunden. - Fehler stehen als Text am Feld und in der Zusammenfassung, nie nur als rote Linie.
- Nach dem Absenden springt der Fokus zur Zusammenfassung oder zum ersten Fehler, der Rest der Eingaben bleibt stehen.
- Auswahl als Chips, Zeiten und Tage nutzt echte Radio- und Checkbox-Felder in einem
fieldsetmitlegend. - Tap-Ziele sind ab 44 Pixel hoch, Telefon, E-Mail und Adresse bringen die passende Tastatur und
autocompletemit. - Ohne JavaScript bleibt jedes Formular lesbar.
MultiStepFormmitactionsendet dann als normales HTML-Formular an deine Adresse.
Mehr zum Prüfen deiner Seiten unter Barrierefreiheit.
Zustände nach dem Absenden#
Ein Formular endet nicht beim Knopf. Die Formularzustände zeigen, was danach passiert, fangen Fehler mit einem Ausweg ab und bewahren halbe Eingaben auf.
<SuccessStates
variant="next-steps"
title="Anfrage ist da"
reference="ANF-2048"
steps={[
{ title: 'Wir prüfen den Termin', state: 'current', when: 'Heute' },
{ title: 'Du bekommst eine Bestätigung', state: 'later', when: 'Morgen' },
]}
primary={{ label: 'Zurück zur Karte', href: '/menu/' }}
/>
<ErrorRecovery
variant="submit-failed"
fallbackEmail="hello@cafe-mira.example"
onSubmit={async (data) => (await fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) })).ok}
/>
<SaveProgress
variant="autosave"
storageKey="application-2026"
fields={[{ name: 'motivation', label: 'Warum diese Stelle', long: true }]}
/>