Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Komponenten-Guides
    4. React Multi Step Form mit zod, Autosave und Animationen
    Read in English
    1. Blog
    2. Komponenten-Guides
    3. React Multi Step Form mit zod, Autosave und Animationen
    Komponenten-Guides
    Komponenten-Guides

    React Multi Step Form mit zod, Autosave und Animationen

    SR
    Serban Rusu · Gründer von Wingo UI
    13. Okt. 2026 · 12 Min. Lesezeit

    Auf dieser Seite

    0%
    1. Was braucht eine gute React Multi Step Form?
    2. Wie teile ich ein zod-Schema in Schritte auf?
    3. Wie validiere ich mit react-hook-form einen Schritt nach dem anderen?
    4. Wie ergänze ich einen async Check in einem Schritt?
    5. Wie speichere ich den Fortschritt über Reloads hinweg?
    6. Wie funktionieren Übergänge, die der Navigationsrichtung folgen?
    7. Brauche ich auf dem Smartphone einen Fortschrittsbalken?
    8. Wie sieht der fertige Code aus?
    9. Kann ich stattdessen eine shadcn Multi Step Form bauen?
    10. Wo fängst du am besten an?
    11. Häufige Fragen
      1. Wie validiere ich in react-hook-form nur den aktuellen Schritt?
      2. Braucht eine Multi Step Form ein useForm oder eins pro Schritt?
      3. Wie speichere ich den Fortschritt einer Multi Step Form über einen Reload hinweg?
      4. Wie füge ich einer React Multi Step Form einen Fortschrittsbalken hinzu?
      5. Hat shadcn/ui eine Komponente für Multi Step Forms?

    Kurz gesagt

    Nutz eine einzige react-hook-form-Instanz für die ganze React Multi Step Form, gib jedem Schritt sein eigenes zod-Objekt und ruf bei „Weiter“ trigger() nur mit den Feldnamen dieses Schritts auf. Lass async Checks wie die Prüfung eines Benutzernamens erst nach den synchronen Regeln laufen, speicher Werte und aktuellen Schritt bei jeder Änderung in localStorage und lass den nächsten Schritt aus der Richtung hereingleiten, in die die Person navigiert. Der Form Wizard von Wingo UI übernimmt Schrittlogik, Fokus und Übergänge; der fertige Code unten ergänzt die zod-Schemas, den async Check und den gespeicherten Entwurf.

    Veröffentlicht am 13. Okt. 2026

    Eine React Multi Step Form sieht aus wie eine Liste von Formularen mit einem Weiter-Button dazwischen. Die eigentliche Arbeit steckt dazwischen: „Weiter“ darf nur die sichtbaren Felder prüfen, ein Server-Check braucht einen Spinner und einen Fehler an der richtigen Stelle, ein Reload soll nicht drei Schritte Tipparbeit wegwerfen, und der nächste Schritt soll aus der Richtung hereingleiten, in die es gerade geht. Dieses Tutorial baut eine Workspace-Registrierung mit vier Schritten, die all das mit react-hook-form, zod und dem Form Wizard von Wingo UI löst, der Bibliothek, die wir bauen. Wie Stepper aussehen, behandelt ein eigener Guide: der Artikel zur React Stepper Komponente.

    Was braucht eine gute React Multi Step Form?

    Sie braucht sieben Dinge: Validierung pro Schritt, async Checks nach den synchronen, Fokus, der der Person folgt, sichtbaren Fortschritt, gespeicherten Fortschritt, Bewegung in Navigationsrichtung und einen einzigen finalen Submit. Das heißt in der Praxis:

    • Validierung pro Schritt. „Weiter“ prüft nur den aktuellen Schritt. „Zurück“ validiert nie, denn niemand sollte erst ein Feld korrigieren müssen, um sich etwas Früheres noch mal anzusehen.
    • Async Checks nach den synchronen. Eine Anfrage wie „Ist diese URL schon vergeben?“ läuft erst, wenn das Format stimmt, und der Button zeigt, dass gerade etwas passiert.
    • Fokus, der der Person folgt. Ein fehlgeschlagener Schritt fokussiert das erste ungültige Feld. Ein neuer Schritt setzt den Fokus auf seine Überschrift, und ein Screenreader liest „Schritt 2 von 4: Workspace“ vor.
    • Sichtbarer Fortschritt. Ein Stepper auf breiten Screens, ein Fortschrittsbalken mit „Schritt 2 von 4“ auf Smartphones.
    • Gespeicherter Fortschritt. Werte und aktueller Schritt überstehen einen Reload oder einen geschlossenen Tab.
    • Bewegung in Navigationsrichtung. Vorwärts gleitet der Inhalt nach links, zurück nach rechts, und mit reduzierter Bewegung wird aus beidem ein Crossfade.
    • Ein Submit. Der letzte Schritt schickt ein einziges Objekt, das der Server noch einmal validiert.

    Der Form Wizard übernimmt den Schritt-State, den Fokus, den Stepper und die Übergänge. Du bringst die Felder, die Schemas und die Speicherung des Entwurfs mit. Installier ihn zusammen mit den Feldern aus diesem Tutorial:

    bash
    npx wingo-ui@latest add form-wizard input radio-group checkbox form-validation

    Die CLI installiert auch react-hook-form, @hookform/resolvers und zod, auf denen die Validierungs-Helfer aufbauen.

    Drück bei einem leeren Schritt auf Weiter: Die Markierung wird zu einem roten „!“, der Button wackelt und der Fokus springt ins erste ungültige Feld. Füll es aus und geh weiter, dann drück Zurück und achte darauf, wie sich die Slide-Richtung umkehrt
    $ npx wingo-ui@latest add form-wizard
    ProDoku: Form Wizard

    Wie teile ich ein zod-Schema in Schritte auf?

    Schreib ein zod-Objekt pro Schritt und führ sie zum Schema für das ganze Formular zusammen. Die Feldnamen eines Schritts kommen dann direkt aus seinem eigenen Schema. Ein neues Feld in einem Schritt wird also von diesem Schritt validiert, ohne dass du eine zweite Liste synchron halten musst. Das ist der Kern einer zod Multi Step Form:

    tsx
    import { z } from "zod";
    import { createValidators } from "@/lib/form-validation";
    const v = createValidators();
    const accountSchema = z.object({
    name: v.name(),
    email: v.email(),
    });
    const workspaceSchema = z.object({
    workspace: v.text({ min: 3, max: 40 }),
    slug: z
    .string()
    .trim()
    .regex(/^[a-z0-9-]{3,30}$/, { error: "Nutze 3 bis 30 Kleinbuchstaben, Ziffern oder Bindestriche" }),
    });
    const teamSchema = z.object({
    teamSize: z.string().min(1, { error: "Wähl eine Teamgröße" }),
    });
    const reviewSchema = z.object({
    terms: v.accept(),
    });
    const signupSchema = z.object({
    ...accountSchema.shape,
    ...workspaceSchema.shape,
    ...teamSchema.shape,
    ...reviewSchema.shape,
    });
    type Signup = z.input<typeof signupSchema>;
    const fieldsOf = (schema: z.ZodObject) => Object.keys(schema.shape) as (keyof Signup)[];

    createValidators kommt aus den Helfern für Formularvalidierung: zod-Regeln für Namen, E-Mails, Passwörter, Telefonnummern und eine angehakte Checkbox, die sich einen Satz englischer Meldungen teilen. Reines zod funktioniert genauso, wenn dir eigene Regeln lieber sind. Die Beispiele nutzen zod 4, dort heißt die Option für die Meldung error.

    Wie validiere ich mit react-hook-form einen Schritt nach dem anderen?

    Nutz ein useForm für das ganze Formular und ruf trigger() mit den Feldern des aktuellen Schritts auf, wenn die Person auf „Weiter“ drückt. Laut der react-hook-form-Doku zu trigger (opens in a new tab) gibt die Funktion ein Promise zurück, das true liefert, wenn die angegebenen Felder gültig sind. Mit dem zod-Resolver läuft trotzdem das ganze Schema, aber nur die genannten Felder bekommen Fehler. Schritt 1 zeigt also nie Meldungen für Schritt 3.

    Erst die eine Formularinstanz macht daraus einen React Hook Form Wizard statt vier einzelner Formulare. Das Wizard-Beispiel (opens in a new tab) der Bibliothek selbst nutzt einen separaten Store zwischen den Seiten, was zu einem Ablauf über mehrere Routen passt. Innerhalb einer Seite brauchst du den nicht: Laut der useForm-Doku (opens in a new tab) ist shouldUnregister standardmäßig false, ein Feld behält also seinen Wert, nachdem sein Schritt unmountet.

    Jeder Schritt registriert seinen Validator mit useFormWizardValidation. Der Wizard führt ihn bei „Weiter“ aus, bei Enter in einem einzeiligen Feld und vor einem Sprung nach vorn im Stepper:

    tsx
    function AccountStep() {
    const { register, trigger, formState } = useFormContext<Signup>();
    useFormWizardValidation(() => trigger(fieldsOf(accountSchema)));
    return (
    <div className="flex flex-col gap-4">
    <Input label="Vollständiger Name" autoComplete="name" error={formState.errors.name?.message} {...register("name")} />
    <Input label="Geschäftliche E-Mail" type="email" autoComplete="email" error={formState.errors.email?.message} {...register("email")} />
    </div>
    );
    }

    Liefert der Validator false, markiert der Wizard den Schritt mit einem Fehler, lässt „Weiter“ wackeln und fokussiert im Schritt das erste Element mit aria-invalid="true". Felder von Wingo UI setzen dieses Attribut, sobald sie einen error bekommen, über die gemeinsame Field-Anatomie. Der Fokus landet also im richtigen Feld, ohne dass du pro Input eine Ref brauchst. Controls, die keine einfachen Inputs sind, etwa eine Radio Group, laufen über Controller und geben fieldState.error?.message an error weiter.

    useZodForm aus derselben Helfer-Datei ist useForm mit dem zod-Resolver und mode: "onTouched": Ein Fehler erscheint, nachdem die Person ein Feld verlassen hat, und aktualisiert sich dann bei jedem Tastendruck, bis er behoben ist.

    Wie ergänze ich einen async Check in einem Schritt?

    Lass zuerst die günstigen Regeln laufen, dann die Anfrage, und schreib die Antwort mit setError ans Feld. Der Validator gibt ein Promise zurück, also hält der Wizard die Person auf dem Schritt, bis es aufgelöst ist:

    tsx
    useFormWizardValidation(async () => {
    if (!(await trigger(fieldsOf(workspaceSchema)))) return false;
    try {
    if (await isSlugFree(getValues("slug"))) return true;
    setError("slug", { message: "Diese URL ist vergeben. Probier eine andere." });
    } catch {
    setError("slug", { message: "Wir konnten diese URL nicht prüfen. Versuch es noch mal." });
    }
    return false;
    });

    Drei Details sind hier wichtig. Die Formatprüfung verhindert, dass ein Tippfehler im Netzwerk landet. Das catch macht aus einer fehlgeschlagenen Anfrage eine Meldung statt einer stillen Blockade. Und der Wizard zeigt einen Spinner auf „Weiter“ erst, wenn ein Validator schon 120ms läuft, damit die sofortigen trigger()-Aufrufe nie einen aufblitzen lassen. Der manuelle Fehler bleibt, bis das Feld erneut validiert wird. Mit mode: "onTouched" passiert das bei jedem Tastendruck erst, nachdem das Feld einmal den Fokus verloren hat. Wer die URL getippt und Enter gedrückt hat, sieht beim Bearbeiten also weiter „vergeben“, bis das Feld per Tab verlassen oder erneut „Weiter“ gedrückt wird. Soll der Fehler beim Tippen sofort verschwinden, registrier das Feld mit register("slug", { onChange: () => clearErrors("slug") }).

    Der Check hier ist nur eine Komfortfunktion. Zwei Personen können dieselbe URL zwischen Check und Submit beanspruchen, also muss der Server die Eindeutigkeit durchsetzen und der finale Submit mit einem Konflikt umgehen können.

    Wie speichere ich den Fortschritt über Reloads hinweg?

    Schreib die Werte und den aktuellen Schritt bei jeder Änderung in localStorage und lies sie nach dem Mount der Komponente wieder aus. Liest du schon beim Rendern, weicht das Server-HTML vom Client ab und die Hydration schlägt fehl. Darum rendert der Wrapper einen Platzhalter mit derselben Höhe, bis der Entwurf bekannt ist:

    tsx
    export function SignupWizard() {
    const [draft, setDraft] = useState<Draft | null>();
    useEffect(() => setDraft(readDraft()), []);
    if (draft === undefined) return <div className="min-h-[480px]" />;
    return <SignupForm draft={draft} />;
    }

    Im Formular abonniert watch(callback) die Änderungen, ohne bei jeder Änderung neu zu rendern. Der aktuelle Schritt ist über value und onValueChange kontrolliert, damit er mit den Werten gespeichert werden kann. Mountet der Wizard auf einem späteren Schritt, zählt er die Schritte davor als erledigt, und der Stepper zeigt sie mit einem Haken.

    Sensible Daten gehören nicht in den Entwurf. Ein Passwort, eine Kartennummer oder eine Ausweisnummer gehört nie in localStorage, wo jedes Script auf der Seite sie lesen kann. Sammelt ein Schritt so etwas, lass das Feld beim Speichern weg und frag es erneut ab.

    Wie funktionieren Übergänge, die der Navigationsrichtung folgen?

    Der nächste Schritt kommt aus der Richtung, in die du navigierst: Vorwärts gleitet er von rechts herein, zurück von links. Der Form Wizard vergleicht den Index des neuen Schritts mit dem alten, verschiebt um 32px in diese Richtung und lässt die Höhe der Card einer Spring folgen, damit der Footer nicht springt. Diese Höhenänderung ist eine Layout-Animation von motion: Sie misst einmal und animiert einen Transform, es läuft also kein Layout pro Frame. Mit prefers-reduced-motion gibt es stattdessen einen Crossfade, und animate={false} tauscht die Schritte ganz ohne Bewegung.

    Wenn du das selbst baust, gibt es genau einen Trick: Gib die Richtung an AnimatePresence weiter, nicht nur an den Schritt. Der Schritt, der verschwindet, ist schon nicht mehr in deinem Baum und bekommt die neue Richtung nur noch über die Prop custom von AnimatePresence:

    tsx
    const slide: Variants = {
    enter: (direction: number) => ({ x: direction * 32, opacity: 0 }),
    center: { x: 0, opacity: 1 },
    exit: (direction: number) => ({ x: direction * -32, opacity: 0 }),
    };
    <AnimatePresence mode="popLayout" initial={false} custom={direction}>
    <motion.div key={index} custom={direction} variants={slide} initial="enter" animate="center" exit="exit">
    {steps[index]}
    </motion.div>
    </AnimatePresence>;

    Animier nur x und opacity. Ein Schrittwechsel, der height oder width direkt animiert, löst in jedem Frame Layout aus, und genau das lässt billig gebaute Wizards auf Mittelklasse-Smartphones ruckeln. Mehr Presets findest du in unserem Artikel zu Spring Presets.

    Brauche ich auf dem Smartphone einen Fortschrittsbalken?

    Ja, unbedingt. Unter 768px passt eine Reihe aus fünf nummerierten Kreisen nicht mit lesbaren Titeln daneben. Bau auf Smartphones die React Multi Step Form mit Fortschrittsbalken und Schrittzähler. Der Form Wizard schaltet selbst um: Unter 768px wird jeder Stepper zu einem kompakten Header mit „Schritt 2 von 4“, dem Titel des Schritts und einem Balken. Auf breiteren Screens wählst du mit stepper: "horizontal" (nummerierte Markierungen), "vertical", "progress", "dots" oder "none".

    Mit fullScreenOnMobile, dem Standard, füllt der Wizard auf dem Smartphone seinen Container: ein Sticky Header, ein Body, der unabhängig scrollt, und ein Sticky Footer mit 48px hohen Buttons und Abstand für den Home-Indikator. Gib dem Container eine Höhe, etwa h-dvh md:h-auto direkt am Wizard. Für einen Stepper außerhalb eines Formulars, zum Beispiel einen Checkout über mehrere Routen, nimm die eigenständige Steps-Komponente, die im Stepper-Guide erklärt wird.

    Wie sieht der fertige Code aus?

    Die ganze Registrierung, rund 270 Zeilen, mit den vier Schritten, dem async URL-Check, der Zusammenfassung mit Bearbeiten-Links und dem gespeicherten Entwurf:

    tsx
    "use client";
    import { useEffect, useState } from "react";
    import { Controller, FormProvider, useFormContext, useWatch } from "react-hook-form";
    import { z } from "zod";
    import { FormWizard, FormWizardSummary, useFormWizardValidation, type WizardStep } from "@/components/ui/form-wizard";
    import { Checkbox } from "@/components/ui/checkbox";
    import { Input } from "@/components/ui/input";
    import { RadioGroup, type RadioOption } from "@/components/ui/radio-group";
    import { createValidators, useZodForm } from "@/lib/form-validation";
    const v = createValidators();
    // ein zod-Objekt pro Schritt
    const accountSchema = z.object({
    name: v.name(),
    email: v.email(),
    });
    const workspaceSchema = z.object({
    workspace: v.text({ min: 3, max: 40 }),
    slug: z
    .string()
    .trim()
    .regex(/^[a-z0-9-]{3,30}$/, { error: "Nutze 3 bis 30 Kleinbuchstaben, Ziffern oder Bindestriche" }),
    });
    const teamSchema = z.object({
    teamSize: z.string().min(1, { error: "Wähl eine Teamgröße" }),
    });
    const reviewSchema = z.object({
    terms: v.accept(),
    });
    // das ganze Formular sind die zusammengeführten Schritte
    const signupSchema = z.object({
    ...accountSchema.shape,
    ...workspaceSchema.shape,
    ...teamSchema.shape,
    ...reviewSchema.shape,
    });
    type Signup = z.input<typeof signupSchema>;
    const EMPTY: Signup = { name: "", email: "", workspace: "", slug: "", teamSize: "", terms: false };
    // die Feldnamen eines Schritts, aus seinem Schema gelesen
    const fieldsOf = (schema: z.ZodObject) => Object.keys(schema.shape) as (keyof Signup)[];
    const TEAM_SIZES: RadioOption[] = [
    { value: "solo", label: "Nur ich" },
    { value: "small", label: "2 bis 10 Personen" },
    { value: "large", label: "Mehr als 10" },
    ];
    function AccountStep() {
    const { register, trigger, formState } = useFormContext<Signup>();
    useFormWizardValidation(() => trigger(fieldsOf(accountSchema)));
    return (
    <div className="flex flex-col gap-4">
    <Input
    label="Vollständiger Name"
    autoComplete="name"
    enterKeyHint="next"
    error={formState.errors.name?.message}
    {...register("name")}
    />
    <Input
    label="Geschäftliche E-Mail"
    type="email"
    autoComplete="email"
    enterKeyHint="next"
    error={formState.errors.email?.message}
    {...register("email")}
    />
    </div>
    );
    }
    async function isSlugFree(slug: string) {
    const response = await fetch(`/api/workspaces/available?slug=${encodeURIComponent(slug)}`);
    if (!response.ok) throw new Error(`Availability check failed: ${response.status}`);
    const data = (await response.json()) as { available: boolean };
    return data.available;
    }
    function WorkspaceStep() {
    const { register, trigger, getValues, setError, formState } = useFormContext<Signup>();
    useFormWizardValidation(async () => {
    // zuerst die billigen Checks, damit ein Tippfehler nie im Netzwerk landet
    if (!(await trigger(fieldsOf(workspaceSchema)))) return false;
    try {
    if (await isSlugFree(getValues("slug"))) return true;
    setError("slug", { message: "Diese URL ist vergeben. Probier eine andere." });
    } catch {
    setError("slug", { message: "Wir konnten diese URL nicht prüfen. Versuch es noch mal." });
    }
    return false;
    });
    return (
    <div className="flex flex-col gap-4">
    <Input
    label="Workspace-Name"
    placeholder="Carter Studio"
    error={formState.errors.workspace?.message}
    {...register("workspace")}
    />
    <Input
    label="Workspace-URL"
    prefix="acme.app/"
    placeholder="carter-studio"
    autoCapitalize="none"
    spellCheck={false}
    error={formState.errors.slug?.message}
    {...register("slug")}
    />
    </div>
    );
    }
    function TeamStep() {
    const { control, trigger } = useFormContext<Signup>();
    useFormWizardValidation(() => trigger(fieldsOf(teamSchema)));
    return (
    <Controller
    name="teamSize"
    control={control}
    render={({ field, fieldState }) => (
    <RadioGroup
    label="Wie viele Personen werden es nutzen?"
    variant="card"
    options={TEAM_SIZES}
    value={field.value}
    onValueChange={field.onChange}
    onBlur={field.onBlur}
    ref={field.ref}
    name={field.name}
    error={fieldState.error?.message}
    />
    )}
    />
    );
    }
    function ReviewStep() {
    const { control, trigger } = useFormContext<Signup>();
    useFormWizardValidation(() => trigger(fieldsOf(reviewSchema)));
    const values = useWatch({ control });
    const team = TEAM_SIZES.find((option) => option.value === values.teamSize)?.label;
    return (
    <div className="flex flex-col gap-5">
    <FormWizardSummary
    sections={[
    {
    step: "account",
    items: [
    { label: "Name", value: values.name },
    { label: "E-Mail", value: values.email },
    ],
    },
    {
    step: "workspace",
    items: [
    { label: "Workspace", value: values.workspace },
    { label: "URL", value: `acme.app/${values.slug}` },
    ],
    },
    { step: "team", items: [{ label: "Teamgröße", value: team }] },
    ]}
    />
    <Controller
    name="terms"
    control={control}
    render={({ field, fieldState }) => (
    <Checkbox
    label="Ich stimme den Nutzungsbedingungen zu"
    checked={field.value}
    onCheckedChange={(checked) => field.onChange(checked === true)}
    onBlur={field.onBlur}
    ref={field.ref}
    error={fieldState.error?.message}
    />
    )}
    />
    </div>
    );
    }
    const STEPS: WizardStep[] = [
    { id: "account", title: "Konto", description: "Wer richtet das ein?", content: <AccountStep /> },
    { id: "workspace", title: "Workspace", description: "Wähl einen Namen und eine URL.", content: <WorkspaceStep /> },
    { id: "team", title: "Team", description: "Danach richten wir die Voreinstellungen aus.", content: <TeamStep /> },
    { id: "review", title: "Übersicht", description: "Prüf alles, dann leg den Workspace an.", content: <ReviewStep /> },
    ];
    // ---------- gespeicherter Fortschritt ----------
    const DRAFT_KEY = "signup-draft";
    type Draft = { step: string; values: Partial<Signup> };
    function readDraft(): Draft | null {
    try {
    const raw = localStorage.getItem(DRAFT_KEY);
    return raw ? (JSON.parse(raw) as Draft) : null;
    } catch {
    return null;
    }
    }
    function writeDraft(draft: Draft) {
    try {
    localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
    } catch {
    // privater Modus oder voller Speicher: das Formular läuft weiter, vergisst nur
    }
    }
    function clearDraft() {
    try {
    localStorage.removeItem(DRAFT_KEY);
    } catch {}
    }
    async function createWorkspace(values: z.output<typeof signupSchema>) {
    const response = await fetch("/api/workspaces", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(values),
    });
    if (!response.ok) throw new Error(`Signup failed: ${response.status}`);
    }
    function SignupForm({ draft }: { draft: Draft | null }) {
    const form = useZodForm(signupSchema, { defaultValues: { ...EMPTY, ...draft?.values } });
    const [step, setStep] = useState(draft?.step ?? "account");
    const [created, setCreated] = useState(false);
    // den Schritt und jede Änderung speichern; der Schritt gehört zum Entwurf
    useEffect(() => {
    writeDraft({ step, values: form.getValues() });
    const subscription = form.watch((values) => writeDraft({ step, values: values as Partial<Signup> }));
    return () => subscription.unsubscribe();
    }, [form, step]);
    if (created) return <p role="status">Dein Workspace ist bereit.</p>;
    return (
    <FormProvider {...form}>
    <FormWizard
    steps={STEPS}
    value={step}
    onValueChange={setStep}
    labels={{ submit: "Workspace anlegen", done: "Angelegt" }}
    onComplete={async () => {
    // wirft bei einem veralteten oder von Hand geänderten Entwurf; der Wizard lässt den Button wackeln
    const values = signupSchema.parse(form.getValues());
    await createWorkspace(values);
    clearDraft();
    setCreated(true);
    }}
    className="h-dvh md:h-auto"
    />
    </FormProvider>
    );
    }
    export function SignupWizard() {
    // undefined, bis der Browser den Entwurf gelesen hat, damit Server- und Client-HTML gleich sind
    const [draft, setDraft] = useState<Draft | null>();
    useEffect(() => setDraft(readDraft()), []);
    if (draft === undefined) return <div className="min-h-[480px]" />;
    return <SignupForm draft={draft} />;
    }

    Zwei Endpoints schreibst du selbst: GET /api/workspaces/available und POST /api/workspaces. Parse den Body auf dem Server mit demselben signupSchema, damit Client- und Server-Regeln nicht auseinanderlaufen können. Schlägt onComplete fehl, lässt der Wizard den Submit-Button wackeln und bleibt auf dem Review-Schritt, und der Entwurf ist weiter gespeichert.

    Die Registrierungsregeln aus form-validation laufen auch für sich, ohne Wizard. Verlass ein Feld, korrigier es und schick dann alles leer ab, um das Timing zu sehen, das useZodForm nutzt:

    Verlass das E-Mail-Feld mit einem Tippfehler: Der Fehler erscheint beim Blur und verschwindet, während du ihn korrigierst. Schick leer ab, und der Fokus springt zum ersten Fehler
    $ npx wingo-ui@latest add form-validation
    ProDoku: Form validation

    Kann ich stattdessen eine shadcn Multi Step Form bauen?

    Ja. Für eine Umfrage oder ein Intake-Formular mit einer Frage pro Screen nimm Questionnaire (opens in a new tab) von shadcn/ui, das im August 2026 (opens in a new tab) dazukam. Es kümmert sich um das aktive Element, den Fortschritt, Zurück und Weiter, Pflichtantworten und den Fokus und liest die Antworten aus FormData. Persistenz und Verzweigungen überlässt die Doku deiner Seite, auf react-hook-form geht sie nicht ein.

    Für Schritte mit jeweils mehreren Feldern, wie die Registrierung hier, enthält die Komponentenliste von shadcn/ui (opens in a new tab) Field, Progress und Button, aber Stand Oktober 2026 keinen Stepper und keinen Wizard. So eine shadcn Multi Step Form hält den Schrittindex im State, rendert pro Schritt eine Gruppe von Feldern, ruft bei „Weiter“ trigger() genau wie oben auf und bringt eigenes Fokus-Handling, eigene Screenreader-Ansagen und eigene Übergänge mit. Community-Registries veröffentlichen Stepper-Komponenten, die sich mit der shadcn CLI installieren lassen, etwa den Stepper von ReUI (opens in a new tab), falls du die Optik willst, ohne sie selbst zu schreiben.

    Die Aufteilung des Schemas, trigger() pro Schritt, der async Check und der Code für den Entwurf funktionieren in diesem Setup genauso, denn nichts davon hängt an der Wizard-Komponente.

    Wo fängst du am besten an?

    Fang mit der Doku zum Form Wizard an. Dort gibt es den Playground und Beispiele für einen vertikalen Stepper, Dots fürs Onboarding, einen async Check und einen eigenen Footer mit „Entwurf speichern“. Der Form Wizard und die Helfer für Formularvalidierung gehören zu Wingo UI Pro, das Stand Oktober 2026 $8 im Monat, $80 im Jahr oder einmalig $150 kostet (Preise); Input, Radio Group, Checkbox und Field sind kostenlos. Für alle anderen Felder im Formular lies den Guide zu React-Formularkomponenten, und stöber durch die übrigen Komponenten-Guides. Einen Checkout, der auf derselben Validierung Schritt für Schritt aufbaut, zeigt das Tutorial zur React Checkout Page.

    Komponenten in diesem Artikel

    • Form Wizard

      A React multi-step form wizard with a stepper, sync or async validation per step, a summary step and a full-screen phone layout.

      Pro
    • Steps

      A React stepper for checkouts, onboarding and KYC: circles, bars or dots, with complete, error and loading states and a phone header.

      Pro
    • Field

      A React form field wrapper every control shares: label, hint, animated error and counter, fieldsets, and the box recipe all inputs use.

      Free
    • Form validation

      Zod validation rules for React forms with one message set (English or Romanian), react-hook-form glue and a pretend request.

      Pro

    Häufige Fragen

    Wie validiere ich in react-hook-form nur den aktuellen Schritt?

    Ruf trigger() mit einem Array der Feldnamen des aktuellen Schritts auf, bevor du weitergehst, und geh nur weiter, wenn das Ergebnis true ist. Mit einem zod-Resolver läuft zwar das ganze Schema, aber react-hook-form meldet Fehler nur für die Felder, die du übergeben hast.

    Braucht eine Multi Step Form ein useForm oder eins pro Schritt?

    Ein useForm für das ganze Formular ist einfacher: Werte bleiben erhalten, wenn ein Schritt unmountet, der Review-Schritt liest alles mit useWatch, und der finale Submit bekommt ein einziges Objekt. react-hook-form behält die Werte unmounteter Felder standardmäßig, weil shouldUnregister false ist.

    Wie speichere ich den Fortschritt einer Multi Step Form über einen Reload hinweg?

    Abonnier Änderungen am Formular mit watch(callback), schreib die Werte und die ID des aktuellen Schritts in localStorage und lies sie erst nach dem Mount wieder aus, damit das Server-HTML weiterhin passt. Speicher nie Passwörter oder Kartennummern und lösch den Entwurf nach einem erfolgreichen Submit.

    Wie füge ich einer React Multi Step Form einen Fortschrittsbalken hinzu?

    Zeig die aktuelle Position als progressbar mit aria-valuenow und einem sichtbaren Label wie „Schritt 2 von 4“. Im Form Wizard von Wingo UI rendert stepper="progress" diesen Header, und auf Smartphones wird jeder Stepper automatisch dazu.

    Hat shadcn/ui eine Komponente für Multi Step Forms?

    Teilweise. Seit August 2026 liefert shadcn/ui Questionnaire, das eine Frage nach der anderen zeigt, mit Fortschritt, Zurück und Weiter sowie der Prüfung von Pflichtantworten. Einen Stepper oder Wizard für Schritte mit jeweils mehreren Feldern gibt es nicht, so eine shadcn Multi Step Form braucht also weiter eigenen Schritt-State oder eine Community-Registry.

    • Formulare
    • React Hook Form
    • Zod
    • Wizard
    • Mobile UI

    Teilen

    SR

    Über den Autor

    Serban Rusu

    Gründer von Wingo UI

    Serban Rusu hat Wingo UI gegründet. Er baut die Komponenten, die CLI und den MCP-Server und schreibt über React-UIs für Smartphones und KI-Agents.

    Mehr von Serban
    Weiterlesen

    Ähnliche Artikel

    Alle Artikel
    Komponenten-Guides

    React Phone Input mit Ländervorwahl und E.164-Wert

    React Phone Input, der beim Tippen pro Land formatiert, das Land aus einer eingefügten +Vorwahl erkennt, mit libphonenumber-js prüft und sauberes E.164 liefert.

    SRSerban Rusu·13. Okt. 2026·8 Min. Lesezeit
    Komponenten-Guides

    CUI prüfen: rumänische Steuer-ID mit TypeScript und Zod

    CUI prüfen in TypeScript: die Prüfziffer mit dem Schlüssel 753217532 Schritt für Schritt, das Präfix RO, eine zod-Regel, ein React-Feld und die ANAF-Abfrage.

    SRSerban Rusu·9. Okt. 2026·10 Min. Lesezeit
    Komponenten-Guides

    React Form Components: barrierefrei, mit Zod und mobil

    React Form Components mit einer Anatomie (Label, Hinweis, Fehler, Zähler), angebunden an react-hook-form und zod und fürs Smartphone gebaut, mit vollem Code.

    SRSerban Rusu·9. Okt. 2026·25 Min. Lesezeit
    Newsletter

    Neue Artikel per E-Mail

    Neue Guides, Tutorials und Vergleiche aus dem Wingo UI Blog, sobald sie erscheinen. Die meisten Artikel sind auf Englisch.

    Kein Spam. Du kannst dich jederzeit abmelden.

    WingoUI

    Animierte, konfigurierbare, mobile-first React-Komponenten und Blöcke. Kopier den Quellcode, pass ihn an und lass deinen Coding-Agenten damit bauen.

    Produkt

    • Komponenten
    • Vorlagen
    • Preise
    • KI-Agenten
    • Theme-Generator

    Ressourcen

    • Doku
    • Blog
    • Changelog
    • Changelog-RSS
    • llms.txt

    Rechtliches

    • AGB
    • Datenschutz
    • Rückerstattung
    • Lizenz
    • Impressum

    Komponenten-Kategorien

    • Buttons & Actions
    • Inputs
    • Forms
    • Navigation
    • Overlays
    • Feedback
    • Data Display
    • Tables & Lists
    • Charts & Stats
    • Layout
    • Media
    • AI Kit
    • Text & Effects
    • Mobile
    • Commerce
    • Marketing Sections
    • Blocks
    • Hooks & Utilities

    © 2026 Wingo UI

    Wingo UI