Zum Inhalt springen
UI Kit
Accent

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.

ComponentVariantenWofür
multi-step-formrail, progress-top, review, accordion, live-summaryLängere Anfragen in Schritten, mit Fortschritt, Prüfseite oder laufender Zusammenfassung.
request-quotebrief, line-items, by-type, call-or-formAngebotsanfragen als Briefing, mit Positionen oder nach Art des Auftrags.
booking-requestevent-venue, flex-dates, packages, date-optionsAnfragen für Termin, Tisch oder Raum, die jemand bestätigt.
callback-requesttime-slots, now-or-later, inline-bar, topic-agentEin Rückruf im gewählten Zeitfenster oder sofort.
application-formjob, quick-apply, guided-letter, eligibilityBewerbungen für Stellen und Programme mit Unterlagen.
registrationattendees, sessions, team, ticketsAnmeldungen für Events, Kurse und Vereine.
file-requestchecklist, single-drop, portal, review-statusUnterlagen von Kunden einsammeln, mit klarer Checkliste.
consent-preferencesemail-topics, unsubscribe, channel-matrix, privacy-requestsEinwilligungen und Kontaktwünsche, die man ändern kann.
address-forminternational, shipping-billing, address-book, lookupAdressen für Lieferung und Rechnung, für jedes Land gesetzt.
payment-formmethods, split-summary, direct-debit, installmentsKarte, Lastschrift und Wallet in einem ruhigen Formular.
surveyscale, matrix, choice, open, branchingUmfragen mit einer Frage je Schritt oder alles auf einer Seite.
success-statesnext-steps, receipt, submitted-summary, status-trackerWas Menschen nach dem Absenden sehen. Nächste Schritte, Beleg, Zusammenfassung, Status.
error-recoverysummary-top, inline-live, submit-failed, session-expiredFehler mit Ausweg, von der Zusammenfassung bis zur abgelaufenen Sitzung.
save-progressautosave, resume-link, drafts, resume-bannerAutomatisch 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.

MultiStepForm, Variante reviewIn Components ansehen
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)}
/>
PropTypStandardWofür
namestring·Schlüssel des Werts im Ergebnis. Auch die id, zu der die Fehlerzusammenfassung springt.
labelstring·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.
requiredboolean·Das Feld muss gefüllt sein. Freiwillige Felder tragen keine Markierung.
options{ value, label, description?, icon? }[]·Auswahl für select, choice und chips.
hintstring·Hilfe unter dem Label, für Screenreader mit dem Feld verbunden.
errorstring·Deine eigene Meldung. Ohne sie sagt das Kit, was fehlt und wofür.
halfboolean·Halbe Breite ab Tablet, zwei halbe Felder teilen sich eine Zeile.
autoCompletestring·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.

src/components/ContactForm.tsx
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-describedby mit 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 fieldset mit legend.
  • Tap-Ziele sind ab 44 Pixel hoch, Telefon, E-Mail und Adresse bringen die passende Tastatur und autocomplete mit.
  • Ohne JavaScript bleibt jedes Formular lesbar. MultiStepForm mit action sendet 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.

ErrorRecovery, Variante summary-topIn Components ansehen
<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 }]}
/>

Oft gesucht

Direkt zu

In die Zwischenablage kopiert