# 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.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 9. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 25 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-form-components-guide
- Englische Fassung: https://wingo-ui.com/blog/react-form-components-guide

## Kurz gesagt

Gute React Form Components teilen eine Anatomie: ein Label, das mit dem Control verknüpft ist, eine Meldungszeile, die den Hinweis oder den Fehler zeigt, und einen Zähler, wenn es ein Limit gibt, mit fertig verdrahteten IDs und aria-Attributen. Verbinde sie mit react-hook-form über register für native Inputs und Controller für alles andere, und validiere mit einem zod-Schema, sobald ein Feld verlassen wurde. Auf dem Smartphone braucht jedes Control 16px Text, die richtige Tastatur, 44px Touch-Ziele und ein Bottom Sheet statt eines Popovers.

Jedes Produkt liefert dieselbe Handvoll Formulare aus: Registrierung, Checkout, Einstellungen, ein Filterpanel, eine Supportanfrage. Die Controls unterscheiden sich, die Bugs wiederholen sich. Ein Label, das nicht mit seinem Input verknüpft ist. Ein Fehler, der nur als roter Rahmen auftaucht. Ein Date Picker, der ein kleines Popover unter deinem Daumen öffnet. Ein selbst gebautes Select, das der Autofill des Browsers überspringt. Dieser Guide behandelt React Form Components als ein Set: welche Controls ein Produkt braucht, welche Anatomie sie teilen sollten, wie du sie mit react-hook-form und zod verdrahtest und was jedes davon auf dem Smartphone tun sollte. Die Beispiele nutzen Wingo UI, die Bibliothek, die wir bauen. Wir sind also nicht neutral; Fakten von außen verlinken auf ihre Quellen und tragen ein Datum.

## Welche Formular-Controls braucht ein Produkt wirklich?

Die fünfzehn Arten von Eingaben in der Tabelle unten decken die Formulare ab, die die meisten Produkte ausliefern. Wähl jede nach dem, was die Person eingibt, und prüf dann, was sie auf dem Smartphone tut. Die meisten React Input Komponenten sehen auf dem Desktop gleich aus. Bei 390px trennen sich die Wege: Ein Popover hat dort keinen Platz, und ein 32px-Ziel ist für einen Daumen zu klein.

| Was eingegeben wird | Komponente | Auf dem Smartphone |
| --- | --- | --- |
| Kurzer Text, E-Mail, URL | [Input](https://wingo-ui.com/components/input) (Free) | 16px Text, Tastatur passend zum `type`, 44px-Box |
| Langer Text | [Textarea](https://wingo-ui.com/components/textarea) (Free) | Wächst Zeile für Zeile, Return macht eine neue Zeile |
| Eine von 2 bis 5 sichtbaren Optionen | [Radio Group](https://wingo-ui.com/components/radio-group), [Segmented Control](https://wingo-ui.com/components/segmented-control) (Free) | 44px-Ziele, bei einem Radio ist die ganze Zeile eins |
| Eine aus einer bekannten Liste | [Select](https://wingo-ui.com/components/select) (Free) | Bottom Sheet mit 48px-Zeilen |
| Eine von Tausenden oder vom Server | [Combobox](https://wingo-ui.com/components/combobox) (Pro) | Sheet in voller Höhe mit eigenem Suchfeld |
| Mehrere aus einer Liste | [Multi-select](https://wingo-ui.com/components/multi-select) (Pro) | Sheet mit einem Footer „Done (3)“ |
| Zustimmung zu den AGB | [Checkbox](https://wingo-ui.com/components/checkbox) (Free) | Die ganze Zeile ist ein 44px-Ziel |
| Eine Einstellung, die sofort greift | [Switch](https://wingo-ui.com/components/switch) (Free) | Lässt sich ziehen wie der iOS-Switch |
| Eine Zahl oder ein Betrag | [Number Input](https://wingo-ui.com/components/number-input), [Currency Input](https://wingo-ui.com/components/currency-input) (Pro) | Ziffern- oder Dezimaltastatur, 44px-Stepper oder Betrags-Chips |
| Ein Datum | [Date Picker](https://wingo-ui.com/components/date-picker) (Pro) | Keine Tastatur: ein Sheet mit 44px-Tagen |
| Eine Uhrzeit | [Time Picker](https://wingo-ui.com/components/time-picker) (Pro) | Scrollräder in einem Sheet |
| Eine Telefonnummer | [Phone Input](https://wingo-ui.com/components/phone-input) (Pro) | Telefontastatur, Länderliste in einem Sheet |
| Ein Einmalcode | [OTP Input](https://wingo-ui.com/components/otp-input) (Free) | Ziffernblock, Code-Autofill |
| Ein Passwort | [Password Input](https://wingo-ui.com/components/password-input) (Pro) | 44px-Button zum Einblenden |
| Dateien und Fotos | [File Upload](https://wingo-ui.com/components/file-upload) (Pro) | „Tap to choose files“, optional ein Kamera-Button |

Im Katalog verteilen sie sich auf zwei Kategorien, [Inputs](https://wingo-ui.com/components/category/inputs) mit 36 Einträgen und [Forms](https://wingo-ui.com/components/category/forms) mit 8, Stand Release 1.7.0. Bei zwei Entscheidungen in dieser Tabelle liegt man leicht daneben, deshalb hier unsere Position.

Für die Auswahl einer Option gibt es drei verschiedene Controls. Wenn Leute die Optionen auf einen Blick vergleichen sollen (Versandart, Tarif, Größe), zeig alle mit einer Radio Group oder einem Segmented Control. Ein Select versteckt die Wahl hinter einem Tap und spart bei vier Optionen nichts. Ein Select passt zu einer bekannten Liste, die man überfliegen kann: Bundesstaaten, Kategorien, ein paar hundert Zeitzonen. Eine Combobox passt, wenn Tippen schneller ist als Scrollen, die Liste von einem Server kommt oder eine freie Antwort erlaubt ist.

Zahlen sind die andere Falle. Ein natives Feld mit `type="number"` ignoriert `maxLength` ([MDN führt das Attribut](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#attributes) nur für Text-, Such-, URL-, Tel-, E-Mail- und Passwortfelder), nimmt Exponenten wie `1e5` an, und in manchen Desktop-Browsern ändert ein Scrollen über dem fokussierten Feld seinen Wert. Unser Input rendert `type="number"` als Textfeld mit Dezimaltastatur. So funktioniert `maxLength`, und ein Scrollen ändert nie den Wert.

## Welche Anatomie sollte jedes Formularfeld teilen?

Jedes Feld sollte dieselben drei Teile in derselben Reihenfolge haben: ein Label über dem Control, die Box des Controls und darunter eine Meldungszeile. Die Meldungszeile zeigt entweder den Hinweis oder den Fehler, nie beides, und am Ende einen Zähler, wenn das Feld ein Limit hat. Wer barrierefreie Formulare baut, vergisst meist genau die Teile, die niemand sieht: die IDs, die Live-Region und das, was ein Screenreader vorliest.

- **Label.** Ein echtes `<label>` mit `htmlFor`, das auf die id des Controls zeigt. Ein Klick darauf fokussiert das Feld, und Screenreader benennen das Control. Die Pflichtmarkierung dahinter ist für Screenreader versteckt, weil das Control selbst `required` trägt. Wenn die meisten Felder Pflicht sind, markier stattdessen die wenigen optionalen.
- **Hinweis.** Hilfetext unter der Box, verknüpft über `aria-describedby`, damit er nach dem Label vorgelesen wird.
- **Fehler.** Er ersetzt den Hinweis. Das `aria-describedby` des Controls wechselt auf die id des Fehlers, `aria-invalid` geht an, und die Box bekommt einen roten Ring plus Icon und Text. [WCAG 2.2, Erfolgskriterium 3.3.1](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html) (Stufe A) verlangt, dass ein automatisch erkannter Fehler benannt und der Person in Textform beschrieben wird. Ein roter Rahmen allein fällt also durch.
- **Zähler.** „12 / 70“ am Ende der Meldungszeile, bernsteinfarben ab 90% und rot am Limit. Der sichtbare Zähler ist für Screenreader versteckt. Sie hören stattdessen „7 characters left“, sobald nur noch 10% oder weniger übrig sind, eine halbe Sekunde nachdem das Tippen aufhört. Die Zahl bei jedem Tastendruck vorzulesen wäre nur Rauschen.

Weil der Fehler den Hinweis ersetzt, statt sich darunter zu stapeln, ändert ein Feld mit Hinweis kaum seine Höhe, wenn der Fehler erscheint. Die Zeile öffnet und schließt sich mit einer kurzen Collapse-Animation, und der Hinweis blendet in den Fehler über. Die Felder darunter gleiten also, statt zu springen. Die Zeile bleibt als Live-Region mit „polite“ gemountet. Ein neuer Fehler wird so einmal angesagt, ohne den Fokus zu stehlen.

In Wingo UI ist diese Anatomie das kostenlose [Field](https://wingo-ui.com/components/field). Du kannst jedes native Control darin einpacken, und deine eigenen Komponenten docken mit einem Hook an:

```tsx
"use client";

import type { ComponentProps } from "react";
import { FIELD_INPUT, Field, FieldAddon, FieldBox, FieldControl, useFieldControl } from "@/components/ui/field";

// jedes native control: Field zeichnet Label und Meldungen, FieldControl verdrahtet ids und aria am input
export function PriceField({ error }: { error?: string }) {
  return (
    <Field label="Preis" description="Der Endpreis inklusive Steuer." error={error} required>
      <FieldBox>
        <FieldControl>
          <input name="price" inputMode="decimal" placeholder="0" autoComplete="off" className={FIELD_INPUT} />
        </FieldControl>
        <FieldAddon>USD</FieldAddon>
      </FieldBox>
    </Field>
  );
}

type PlainInputProps = ComponentProps<"input"> & { label?: string; description?: string; error?: string };

// dein eigenes control: ein hook, und es funktioniert allein oder in einem <Field>
export function PlainInput({ id, label, description, error, required, disabled, readOnly, ...props }: PlainInputProps) {
  // die flags laufen über den hook: controlProps setzt id, required, disabled und readOnly am input
  const field = useFieldControl({ id, label, description, error, required, disabled, readOnly });
  return field.wrap(
    <FieldBox invalid={field.invalid} disabled={field.disabled} readOnly={field.readOnly}>
      <input {...props} {...field.controlProps} className={FIELD_INPUT} />
    </FieldBox>,
  );
}
```

`FieldControl` setzt die id des Fields, `aria-describedby`, `aria-invalid` und `aria-required` auf sein Kind, und die eigenen Props des Kinds gewinnen. `useFieldControl` liefert dieselben IDs und Flags für ein eigenes Control, dazu `wrap()`. Das zeichnet Label und Meldungszeile um das Control, wenn es ein eigenes Label oder einen eigenen Fehler hat, und tut nichts, wenn ein übergeordnetes Field das schon erledigt. Gib `required`, `disabled` und `readOnly` über den Hook weiter, nicht direkt ans Input: `controlProps` enthält diese Keys immer. Steht sein Spread hinter deinen eigenen Props, setzt er sie sonst auf `undefined` zurück.

> Live-Demo (Field): Drück Continue mit einem Titel unter 10 Zeichen: Der Fehler ersetzt den Hinweis, und die Felder darunter gleiten nach unten; tipp weiter, und er verschwindet wieder. Probier sie unter [Field](https://wingo-ui.com/components/field) aus und installier sie mit `npx wingo-ui@latest add field`.

Die Demo ist ein Inseratsformular mit einem Titel von 70 Zeichen, einem Pflichtpreis mit USD-Suffix und einer Stadt. Der Zähler wird kurz vor dem Limit bernsteinfarben, und der Continue-Button setzt den Fokus auf das erste Feld, das noch korrigiert werden muss.

Bibliotheken unterscheiden sich darin, wie viel von dieser Verdrahtung sie dir abnehmen. Stand Oktober 2026 ist das [Field von shadcn/ui](https://ui.shadcn.com/docs/components/field) ein Satz von Layout-Teilen: Du verbindest `FieldLabel htmlFor` selbst mit der id des Inputs, setzt `data-invalid` auf das Field und `aria-invalid` auf das Control und gibst Meldungen an `FieldError`, das auch ein `errors`-Array aus react-hook-form annimmt. [useField von React Aria](https://react-aria.adobe.com/useField) geht den anderen Weg: Laut Doku erzeugt der Hook die IDs für jedes Element selbst und verknüpft sie mit den passenden ARIA-Attributen. Unser Field liegt näher an React Aria: Das Control liest IDs und Flags aus dem Context. Der explizite Ansatz von shadcn hat aber einen echten Vorteil: Es gibt keinen versteckten Context, den man debuggen muss. Wenn dein Team ohnehin mit shadcn/ui arbeitet, ist das eine gute Wahl. Wir haben uns für Context entschieden, weil 31 unserer Controls dieselbe `error`-Prop nehmen und jedes Mal dasselbe Markup erzeugen müssen.

## Warum sollte jedes Control die Props label, description und error nehmen?

Weil sich ein Formular dann wie eine flache Liste von Feldern liest und der Tausch eines Controls gegen ein anderes nie die Verdrahtung ändert. In Wingo UI nehmen 31 Controls, Stand Release 1.7.0 (Input, Textarea, Select, Combobox, Phone Input, die Date Picker, Checkbox, Switch, Radio Group, Slider und der Rest), direkt `label`, `description` und `error`, die meisten auch `required`, `invalid`, `disabled` und `readOnly`, und rendern die Field-Anatomie selbst:

```tsx
"use client";

import { useState } from "react";
import { DatePicker } from "@/components/ui/date-picker";
import { Input } from "@/components/ui/input";
import { PhoneInput } from "@/components/ui/phone-input";
import { Select } from "@/components/ui/select";

const STATES = [
  { value: "CA", label: "Kalifornien" },
  { value: "NY", label: "New York" },
  { value: "TX", label: "Texas" },
];

export function SameAnatomy() {
  const [title, setTitle] = useState("");
  return (
    <div className="flex flex-col gap-4">
      <Input
        label="Titel des Inserats"
        description="Marke, Modell und Zustand."
        value={title}
        onValueChange={setTitle}
        error={title && title.length < 10 ? "Der Titel braucht mindestens 10 Zeichen." : undefined}
        showCount
        maxLength={70}
        required
      />
      <Select label="Bundesstaat" options={STATES} description="Wir liefern nur in diese Bundesstaaten." required />
      <PhoneInput label="Telefon" defaultCountry="US" description="Nur für Updates zur Lieferung." />
      <DatePicker label="Liefertermin" locale="en-US" error="Sonntags liefern wir nicht." />
    </div>
  );
}
```

Vier verschiedene Controls, eine Struktur. Alles, was das Field über diese Props hinaus nimmt (eine Markierung für optionale Felder, eine Aktion am Label wie „Passwort vergessen?“, ein horizontales Layout mit Label-Spalte), läuft über `fieldProps`. Das akzeptieren Input, Select, Phone Input, Date Picker und die meisten anderen Controls. Abschnitte eines Formulars kommen in eine `FieldGroup`. Sie ist ein echtes `<fieldset>` mit `<legend>`, damit ein Screenreader „Lieferadresse“ vor dem ersten Feld der Gruppe ansagt. Eine deaktivierte FieldGroup deaktiviert jedes Feld darin, und ihre Prop `columns` ergibt eine Spalte auf dem Smartphone und zwei ab 640px.

Zwei Details dieser Anatomie ersparen echte Bugs. Ein Klick auf das Label eines Selects fokussiert den Trigger, ohne ihn anzuklicken. Das Wort „Bundesstaat“ klappt also nie eine Liste auf. Und die Prop `name` sendet bei jedem dieser vier Controls einen Wert über ein natives oder verstecktes Input. Das ist wichtiger, als es klingt; weiter unten gibt es einen eigenen Abschnitt dazu.

## Wie verbindest du react-hook-form und zod mit diesen Komponenten?

Nimm `register` für Controls, die ein natives Input rendern und das native `onChange`, `onBlur` und `ref` weitergeben, und `Controller` für alles andere. Validiere mit einem zod-Schema, dessen Regeln sich einen Satz Meldungen teilen, und zeig Fehler, sobald ein Feld verlassen wurde. Es gibt keine speziellen React Hook Form Komponenten, die du installieren musst: Wenn eine Komponente diese drei Dinge an ihr fokussierbares Element weitergibt, kann RHF sie steuern.

Die Lib [form-validation](https://wingo-ui.com/components/form-validation) bündelt den Glue-Code für Formulare, den unsere Blöcke teilen. `createValidators()` liefert zod-Regeln mit einem Satz Meldungen (`text`, `name`, `email`, `password`, `phone`, `url`, `otp`, `accept` für eine angehakte Checkbox, `number`, dazu die rumänischen `cui`, `iban` und `postalCode`). `useZodForm(schema)` ist `useForm` mit dem zod-Resolver und unserem Timing. `fieldError(form, "email")` macht aus dem Fehler eines Felds den String, den die Prop `error` einer Komponente annimmt. Hier ein Formular für eine Lieferanfrage mit sieben Feldern:

```tsx
"use client";

import { Controller } from "react-hook-form";
import { z } from "zod";
import { addDays, startOfToday } from "date-fns";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { DatePicker } from "@/components/ui/date-picker";
import { Input } from "@/components/ui/input";
import { PhoneInput } from "@/components/ui/phone-input";
import { Select, type SelectOption } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { createValidators, fieldError, useZodForm } from "@/lib/form-validation";

const v = createValidators();

const STATES: SelectOption[] = [
  { value: "CA", label: "Kalifornien", keywords: ["CA"] },
  { value: "NY", label: "New York", keywords: ["NY"] },
  { value: "TX", label: "Texas", keywords: ["TX"] },
  { value: "WA", label: "Washington", keywords: ["WA"] },
];

const schema = z.object({
  name: v.name(),
  email: v.email(),
  phone: v.phone({ country: "US" }),
  state: v.text(),
  deliveryDate: z.date({ error: v.messages.required }),
  notes: v.text({ required: false, max: 200 }),
  terms: v.accept(),
});

export type DeliveryRequest = z.output<typeof schema>;

export function DeliveryForm({ onSave }: { onSave: (values: DeliveryRequest) => Promise<void> }) {
  const form = useZodForm(schema, {
    defaultValues: { name: "", email: "", phone: "", state: "", notes: "", terms: false },
  });

  return (
    <form onSubmit={form.handleSubmit(onSave)} noValidate className="flex flex-col gap-4">
      {/* native inputs darin: register reicht */}
      <Input label="Vollständiger Name" autoComplete="name" required {...form.register("name")} error={fieldError(form, "name")} />
      <Input
        label="E-Mail"
        type="email"
        autoComplete="email"
        required
        {...form.register("email")}
        error={fieldError(form, "email")}
      />

      {/* alles andere: Controller, mit dem ref, damit ein gescheitertes absenden es fokussieren kann */}
      <Controller
        control={form.control}
        name="phone"
        render={({ field }) => (
          <PhoneInput
            label="Telefon"
            defaultCountry="US"
            preferredCountries={["US", "CA"]}
            required
            ref={field.ref}
            name={field.name}
            value={field.value}
            onValueChange={(e164) => field.onChange(e164)}
            onBlur={field.onBlur}
            error={fieldError(form, "phone")}
          />
        )}
      />
      <Controller
        control={form.control}
        name="state"
        render={({ field }) => (
          <Select
            label="Bundesstaat"
            placeholder="Wähl den Bundesstaat"
            options={STATES}
            required
            ref={field.ref}
            name={field.name}
            value={field.value}
            onValueChange={field.onChange}
            onBlur={field.onBlur}
            error={fieldError(form, "state")}
          />
        )}
      />
      <Controller
        control={form.control}
        name="deliveryDate"
        render={({ field }) => (
          <DatePicker
            label="Liefertermin"
            locale="en-US"
            min={addDays(startOfToday(), 1)}
            disabled={{ dayOfWeek: [0] }}
            required
            ref={field.ref}
            name={field.name}
            value={field.value ?? null}
            onValueChange={(date) => field.onChange(date ?? undefined)}
            onBlur={field.onBlur}
            error={fieldError(form, "deliveryDate")}
          />
        )}
      />
      <Controller
        control={form.control}
        name="notes"
        render={({ field }) => (
          <Textarea
            label="Hinweise für den Fahrer"
            maxLength={200}
            showCount
            ref={field.ref}
            name={field.name}
            value={field.value}
            onValueChange={field.onChange}
            onBlur={field.onBlur}
            error={fieldError(form, "notes")}
          />
        )}
      />
      <Controller
        control={form.control}
        name="terms"
        render={({ field }) => (
          <Checkbox
            label="Ich akzeptiere die Lieferbedingungen"
            ref={field.ref}
            name={field.name}
            checked={field.value === true}
            onCheckedChange={(checked) => field.onChange(checked === true)}
            onBlur={field.onBlur}
            error={fieldError(form, "terms")}
          />
        )}
      />
      <Button type="submit" size="lg" fullWidth loading={form.formState.isSubmitting}>
        Lieferung anfragen
      </Button>
    </form>
  );
}
```

Installier die Teile mit einem Befehl; die CLI bringt react-hook-form, `@hookform/resolvers`, zod, libphonenumber-js und date-fns mit:

```bash
npx wingo-ui@latest add form-validation input select phone-input date-picker textarea checkbox
```

Phone Input, Date Picker und die form-validation-Lib sind Pro: Führ einmal `npx wingo-ui@latest login` mit einem Pro-Konto aus, sonst hält die CLI an und sagt dir, dass du dich anmelden sollst. Der Code nutzt zod 4 und react-hook-form 7. Ein paar Zeilen darin tragen mehr Gewicht, als man ihnen ansieht.

### Warum validieren, sobald ein Feld verlassen wurde?

`useZodForm` setzt `mode: "onTouched"` und `reValidateMode: "onChange"`. Die [Doku von React Hook Form](https://react-hook-form.com/docs/useform) beschreibt onTouched so: Die Validierung startet beim ersten Blur und läuft danach bei jeder Änderung. So hört niemand nach dem ersten Buchstaben, dass seine E-Mail ungültig ist, und sobald ein Fehler sichtbar ist, verschwindet er mit dem Tastendruck, der ihn behebt. Dieselbe Doku warnt, dass `mode: "onChange"` oft spürbar auf die Performance geht. Das ist ein zweiter Grund, ihn bei langen Formularen zu meiden.

### Warum field.ref an jeden Controller geben?

`shouldFocusError` ist standardmäßig an. Ein gescheitertes Absenden setzt also den Fokus auf das erste Feld mit Fehler, aber nur, wenn RHF einen ref auf etwas Fokussierbares hält. Die [Doku zu Controller](https://react-hook-form.com/docs/usecontroller/controller) sagt, du sollst `field.ref` zuweisen, damit Hook Form das fehlerhafte Input fokussieren kann. In diesen Komponenten ist `ref` eine normale Prop (React 19, kein `forwardRef`), und sie landet auf dem Element, das eine Person fokussieren würde: auf dem Trigger-Button des Selects, dem Nummernfeld des Phone Inputs, dem Textfeld des Date Pickers, dem Button der Checkbox. Unser [Guide zur forwardRef-Migration in React 19](https://wingo-ui.com/de/blog/react-19-forwardref-ref-as-prop) zeigt, wie du deine eigenen Felder auf dieses Muster umstellst.

### Wann reicht register nicht?

Wenn der Wert etwas anderes ist als der String in einem nativen Input: eine Telefonnummer in E.164, ein `Date`, ein Boolean, ein Wert aus einer Liste. Und wenn ein Textfeld einen Zähler zeigt. Input und Textarea halten ihre Länge in eigenem State. Ein natives Zurücksetzen des Formulars bekommen sie mit, aber ein Wert, den RHF mit `setValue()` oder `reset(values)` direkt ins DOM schreibt, erreicht den Zähler erst, wenn das Feld wieder den Fokus bekommt. Das Feld für die Fahrerhinweise oben läuft aus diesem Grund über `Controller`, obwohl eine native Textarea darin steckt.

### Was ist mit dem Datum und der Telefonnummer?

Der Date Picker meldet `null`, wenn er geleert wird. Das Formular speichert deshalb `undefined`, und `z.date({ error })` schlägt mit der Pflichtmeldung fehl. Ein getipptes Datum erreicht das Formular erst, wenn es vollständig und erlaubt ist; ein getippter Sonntag also nie. Der Phone Input meldet einen E.164-String wie `+12015550123`, sobald die Nummer möglich ist, und davor einen leeren String. `v.phone({ country: "US" })` prüft ihn mit libphonenumber-js. Das Land zählt nur für Werte ohne Ländervorwahl, etwa eine Nummer in einem einfachen Input. Es steht standardmäßig auf `RO`, also setz es in einem US-Formular.

`noValidate` am Formular schaltet die eigenen Sprechblasen des Browsers ab. So sind die zod-Meldungen die einzigen, die Leute sehen. Ohne das blockiert der Browser bei einem leeren Feld mit `required` das Absenden mit seinem eigenen Tooltip, und `handleSubmit` läuft nie, um deine Meldungen zu zeigen.

> Live-Demo (Form validation): Schick das leere Formular ab: Alle Fehler erscheinen auf einmal, und der Fokus springt zum ersten; korrigier ein Feld, und sein Fehler verschwindet beim Tippen. Probier sie unter [Form validation](https://wingo-ui.com/components/form-validation) aus und installier sie mit `npx wingo-ui@latest add form-validation`.

Die Demo ist ein Registrierungsformular auf Basis derselben Lib: Name, E-Mail, Telefon, eine optionale Steuernummer der Firma, ein Passwort mit Stärkeregel und eine AGB-Checkbox.

### Sollte der Server dasselbe Schema prüfen?

Ja, immer. Das Schema im Client ist eine Bequemlichkeit für Menschen, und alles, was deine Server Action oder deine API-Route erreicht, muss erneut geprüft werden. Der [Forms-Guide von Next.js](https://nextjs.org/docs/app/guides/forms) empfiehlt, die gesendeten Werte in der Server Action mit einer Schema-Bibliothek wie zod zu parsen. Einen Haken hat unsere Lib derzeit: `lib/form-validation.ts` beginnt mit `"use client"`, weil sie den Hook `useZodForm` enthält. Server-Code kann `createValidators()` daraus also nicht aufrufen. Schreib das Server-Schema direkt mit zod, oder verschieb die Regeln in ein Modul ohne die Direktive; nach der Installation ist die Datei ja dein Quellcode.

## Brauchst du react-hook-form überhaupt?

Für ein kurzes React Formular nicht. Wenn jedes Control über ein echtes Input mit `name` sendet, hält das `FormData` des Browsers schon saubere Werte. Pflichtfelder blockieren das Absenden nativ, ein Zurücksetzen stellt die Standardwerte wieder her, und Autofill funktioniert. Deshalb rendern die Controls in diesem Guide etwas Natives, sobald du `name` übergibst:

- **Select** rendert ein verstecktes natives `<select>`. So funktionieren `FormData`, `required`, das Zurücksetzen und der Autofill des Browsers. [MDN weist darauf hin](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete), dass Browser vor dem Autofill unter Umständen einen `name` oder eine `id` und ein umschließendes Formular verlangen. Genau das fehlt einem Select aus divs. Gib `autoComplete="address-level1"` mit (der Bundesstaat in einer US-Adresse), und der Browser kann es aus einer gespeicherten Adresse füllen.
- **Phone Input** rendert ein verstecktes Input mit der E.164-Nummer, während das sichtbare Input die formatierten nationalen Ziffern zeigt.
- **Date Picker** rendert ein verstecktes Input mit dem lokalen ISO-Datum (`2026-10-23`, oder `2026-10-23T14:30` mit Uhrzeit), nie nach UTC verschoben. Das vermeidet den Off-by-one-Fehler um einen Tag, den `toISOString()` östlich von UTC verursacht.
- **Checkbox** sendet ihren `value` über das versteckte Input von Radix, solange sie angehakt ist; **Input** und **Textarea** sind native Elemente.

```tsx
"use client";

import type { FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { DatePicker } from "@/components/ui/date-picker";
import { PhoneInput } from "@/components/ui/phone-input";
import { Select } from "@/components/ui/select";

const STATES = [
  { value: "CA", label: "Kalifornien" },
  { value: "NY", label: "New York" },
  { value: "TX", label: "Texas" },
];

export function QuoteForm({ onQuote }: { onQuote: (values: Record<string, FormDataEntryValue>) => void }) {
  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    // { phone: "+12015550123", state: "NY", pickup: "2026-10-23" }
    onQuote(Object.fromEntries(new FormData(event.currentTarget)));
  };

  return (
    <form onSubmit={submit} className="flex flex-col gap-4">
      <PhoneInput name="phone" label="Telefon" defaultCountry="US" required />
      <Select name="state" label="Bundesstaat" options={STATES} autoComplete="address-level1" required />
      <DatePicker name="pickup" label="Abholdatum" locale="en-US" />
      <Button type="submit">Angebot anfordern</Button>
    </form>
  );
}
```

Dieselben versteckten Inputs bekommt auch eine Server Action, wenn du sie an `<form action>` übergibst, denn die Action wird mit dem `FormData` des Formulars aufgerufen. Unsere Faustregel: einfaches `FormData` für Formulare mit bis zu fünf oder sechs Feldern und einfachen Regeln. react-hook-form, sobald du Regeln über mehrere Felder brauchst (ein Passwort und seine Bestätigung, was `withMatch` in der Lib erledigt), Fehler, die sich beim Tippen aktualisieren, Dirty Tracking für einen Hinweis „Du hast ungespeicherte Änderungen“ oder Listen von Feldern, die Leute hinzufügen und entfernen.

## Wie sollte sich jedes Control auf dem Smartphone verhalten?

Felder, in die man tippt, bekommen 16px Text, die richtige Tastatur und die richtige Enter-Taste. Felder, aus denen man wählt, öffnen ein Bottom Sheet statt eines Popovers. Nichts, worauf man tippt, hat eine Trefferfläche unter 44px. Das ganze Regelwerk steht in unserem Guide zu [Mobile-first React Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components); hier siehst du, wie es Control für Control greift.

### Textfelder: 16px, die Tastatur und die Enter-Taste

Safari auf iOS zoomt die Seite, wenn du ein Feld fokussierst, dessen Text kleiner als 16px ist, und zoomt beim Blur nicht wieder heraus ([der Artikel von Rick Strahl](https://weblog.west-wind.com/posts/2023/Apr/17/Preventing-iOS-Textbox-Auto-Zooming-and-ViewPort-Sizing) hat die Details). Die Lösung ist die Schriftgröße. Verzichte auf `maximum-scale=1` im Viewport-Meta-Tag: Derselbe Artikel merkt an, dass Android es beachtet und Pinch-Zoom blockiert. Das schadet den Leuten, die aufs Zoomen angewiesen sind. Jede Feldbox von Wingo UI startet bei `text-base` (16px) und wechselt ab 768px auf ihre Desktop-Größe: `text-base md:text-sm` gibt der md-Box dort 14px.

Die Tastatur folgt `type` und `inputMode`: `email` ergänzt die @-Taste, `tel` zeigt die Telefontastatur, `decimal` zeigt Ziffern und ein Trennzeichen. `autoComplete` mit dem passenden Token (`name`, `email`, `tel-national`, `new-password`, `postal-code`) lässt den Browser das Feld füllen, und `enterKeyHint="next"` auf jedem Feld außer dem letzten (dort `"done"`) beschriftet die Enter-Taste mit dem, was sie tun wird. Die md-Box ist mit Maus 40px hoch und auf Touchscreens 44px, über Tailwinds Variante `pointer-coarse`, und der Button zum Leeren behält eine 44px-Trefferfläche, auch wenn er klein aussieht.

### Select und Combobox: ein Sheet und eine Tastatur, die wartet

Unter 768px öffnet das Select ein Bottom Sheet mit dem Label des Felds als Titel, mit 48px-Zeilen und der gewählten Zeile im sichtbaren Bereich. Ein Tap wählt und schließt. Listen mit mehr als acht Optionen bekommen oben eine Suchzeile. Auf einem Touchscreen wartet sie aber auf einen Tap, bevor sie den Fokus nimmt. So verdeckt die Tastatur nicht die halbe Liste, die du gerade scrollen wolltest. Die Combobox macht absichtlich das Gegenteil: Ein Tap öffnet ein Sheet in voller Höhe mit bereits offener Tastatur, weil Tippen der ganze Sinn einer Combobox ist. Gib `responsive={false}` mit, um auf dem Smartphone beim Popover zu bleiben, etwa in einem Sheet, das schon den ganzen Bildschirm füllt.

### Datum und Uhrzeit: keine Tastatur

Auf dem Desktop ist ein Datum schneller getippt als geklickt. Der Date Picker liest „09232026“ als 09/23/2026, „+3“ als drei Tage ab heute und „tomorrow“ als morgen, und er formatiert den Text so, wie die Locale ihn schreibt. Auf dem Smartphone öffnet das Feld nie die Tastatur. Ein Tap öffnet ein Sheet mit einem Monat aus 44px-Tagen, zwischen denen du wischen kannst. Schaltest du `presets` ein, kommt eine Reihe Chips dazu (Today, Tomorrow, Next week oder deine eigenen). Mit `withTime` öffnet das Zeitfeld im Sheet ein eigenes Sheet mit Scrollrädern. Der Time Picker nutzt dieselben Räder. Sie rasten ein wie der iOS-Picker und geben pro Zeile ein kurzes Vibrationssignal, wo der Browser die Vibration API unterstützt.

### Telefonnummern: ein sauberer Wert

Der Phone Input öffnet die Telefontastatur, formatiert die Nummer so, wie das Land sie schreibt, und hält den Cursor hinter der getippten Ziffer. Fügst du eine Nummer ein, die mit `+44` oder `0044` beginnt, oder füllt Autofill sie ein, wechselt das Land von selbst. Der Flaggen-Button öffnet die Länderliste, auf dem Smartphone ein Bottom Sheet mit Suche, und du kannst nach Ländername, ISO-Code oder Vorwahl suchen. Egal, was die Person tippt: Dein Formular bekommt einen E.164-String.

> Live-Demo (Phone Input): Tipp eine US-Nummer und dann eine, die mit +44 beginnt, oder füg sie ein: Die Flagge wechselt zum Vereinigten Königreich, und die Anzeige zeigt den sauberen Wert, den das Formular sendet. Probier sie unter [Phone Input](https://wingo-ui.com/components/phone-input) aus und installier sie mit `npx wingo-ui@latest add phone-input`.

### Einmalcodes: zuerst Autofill

Der [OTP Input](https://wingo-ui.com/components/otp-input) setzt `autocomplete="one-time-code"` auf sein erstes Feld. [MDN beschreibt](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete) diesen Token als Einmalpasswort zur Bestätigung der Identität, also kann der Browser einen Code anbieten, den er per SMS bekommen hat. Die Felder öffnen den Ziffernblock, ein eingefügter Code füllt alle Felder auf einmal, und Backspace leert ein Feld, bevor es zurückspringt.

Er ist auch das einzige Control in diesem Guide, das nicht an die Field-Anatomie andockt: Er rendert eine Reihe von Inputs mit je einem Zeichen, jedes benannt als „Digit 1 of 6“ und so weiter, und sein Fehlerzustand ist ein roter Ring und ein Schütteln. Gib der Reihe einen Gruppennamen und schreib den Fehler als Text darunter, mit `FieldMessage`, das auch außerhalb eines Fields funktioniert:

```tsx
"use client";

import { useState } from "react";
import { FieldMessage } from "@/components/ui/field";
import OtpInput, { type OtpStatus } from "@/components/ui/otp-input";

export function VerifyPhone({ phone, verify }: { phone: string; verify: (code: string) => Promise<boolean> }) {
  const [status, setStatus] = useState<OtpStatus>("idle");

  return (
    <div className="flex flex-col gap-3">
      <p id="otp-title" className="text-sm text-muted-foreground">
        Gib den 6-stelligen Code ein, den wir an {phone} geschickt haben.
      </p>
      <OtpInput
        role="group"
        aria-labelledby="otp-title"
        length={6}
        status={status}
        autoFocus
        onChange={() => setStatus("idle")}
        onComplete={async (code) => setStatus((await verify(code)) ? "success" : "error")}
      />
      <FieldMessage error={status === "error" ? "Der Code stimmt nicht. Sieh in der neuesten SMS nach." : undefined} />
    </div>
  );
}
```

Hier lassen wir react-hook-form weg. Ein Code-Screen hat einen Wert und eine Regel, und die Antwort kommt vom Server. `onComplete` und ein Status sind also der ganze State, den er braucht. Wenn du es doch brauchst, verdrahtet unser [Guide zum React OTP Input](https://wingo-ui.com/de/blog/react-otp-input) den Code mit react-hook-form und zod, erklärt, warum SMS-Autofill ohne Fehlermeldung scheitert, und ergänzt einen Resend-Timer.

### Zahlen und Geld

Number Input und Currency Input lesen Zahlen so, wie die Locale sie schreibt: 1,234.5 auf Englisch, 1.234,5 auf Rumänisch. Der Number Input öffnet den Ziffernblock, oder die Dezimaltastatur, wenn `step` oder `decimals` Nachkommastellen erlauben. Er lehnt Tastendrücke ab, die nie eine gültige Zahl werden können (ein Minus, wenn `min` 0 oder größer ist), und hat Stepper mit 44px-Trefferfläche auf Touch. Der Currency Input füllt die Cents beim Blur auf („1.234,5“ wird zu „1.234,50“) und zeigt über `presets` schnelle Beträge als Chips. Bei Trinkgeld und Spenden sind sie schneller als Tippen.

Teste negative Zahlen auf einem echten Smartphone: [MDN merkt an](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inputmode), dass Geräte bei den Input-Modi `numeric` und `decimal` eine Minustaste zeigen können oder auch nicht. Fehlt sie bei deinem, gib dem Number Input `inputMode="text"` mit oder verlass dich auf die Stepper.

## Wann sollte ein Formular seine Fehler zeigen?

Zeig einen Fehler, nachdem die Person das Feld verlassen hat, aktualisier ihn live, während sie ihn behebt, und zeig beim Absenden alle Fehler auf einmal und setz den Fokus auf den ersten. Das ist das `onTouched`-Timing von oben, und es passt zu fast jedem Formular. Ein paar Regeln drumherum:

- **Nie beim Laden.** Ein leeres Pflichtfeld ist kein Fehler, solange niemand die Chance hatte, es auszufüllen.
- **Lass den Absenden-Button aktiv.** Ein deaktivierter Button nennt keinen Grund, und Tab überspringt ihn, sodass Tastaturnutzer ihn nicht einmal erreichen. Lass das Absenden scheitern und zeig die Fehler.
- **Prüf Langsames, ohne zu blockieren.** Ein Input mit `loading` zeigt am Ende der Box einen Spinner (anstelle des Icons rechts, falls es eins hat), setzt `aria-busy` und bleibt bearbeitbar. Eine Prüfung des Benutzernamens läuft also, während die Person weitertippt.
- **Schreib Fehler als Anweisungen.** „Gib eine gültige E-Mail-Adresse ein“ sagt Leuten, was sie tun sollen; „Ungültige Eingabe“ lässt sie raten. Die meisten englischen Meldungen der Lib folgen diesem Muster, und `createValidators(VALIDATION_MESSAGES_RO)` tauscht mit einer Zeile den rumänischen Satz ein.

## Was sollte dir eine Bibliothek für React Form Components bieten?

Eine Bibliothek für React Form Components hat ihren Platz verdient, wenn sie dir die Verdrahtung abnimmt, die du sonst in jedem Feld wiederholen würdest, und dich ändern lässt, was sie falsch gemacht hat. Unsere Checkliste, die du auf jede Bibliothek anwenden kannst, unsere eingeschlossen (Preise und Lizenzen findest du in unserem [Vergleich der shadcn/ui-Alternativen](https://wingo-ui.com/de/blog/shadcn-alternatives)):

1. **Eine Anatomie für jedes Control**, mit fertig verdrahteten IDs für Label, Hinweis, Fehler und Zähler, und ein Hook, mit dem deine eigenen Controls andocken.
2. **Native Teilnahme am Formular.** Ein `name`, der im `FormData` landet, `required`, das das Absenden blockiert, ein Zurücksetzen, das die Standardwerte wiederherstellt, und Autofill, der auch eigene Selects erreicht.
3. **Kontrollierter und unkontrollierter Modus**, und ein `ref`, der auf dem Element landet, das eine Person fokussieren würde. So kann eine Formular-Bibliothek den ersten Fehler fokussieren.
4. **Smartphone-Verhalten in derselben Komponente**: 16px Text, Tastaturen, 44px-Ziele, Bottom Sheets unter 768px. Eine separate Mobile-Komponente verdoppelt jedes Formular.
5. **Quellcode, der dir gehört.** Wingo UI kopiert die Dateien in dein Projekt, wie shadcn/ui. Du kannst also ein Label, ein Timing oder eine Schema-Regel ändern, ohne auf ein Release zu warten, und `npx wingo-ui@latest update` führt unsere späteren Fixes per Merge in die Dateien ein, die du bearbeitet hast. Denselben 3-Way-Merge von Hand mit git, für Dateien aus der shadcn-CLI, zeigt unser Guide zum [Aktualisieren von shadcn-Komponenten, ohne Änderungen zu verlieren](https://wingo-ui.com/de/blog/update-shadcn-components-without-losing-edits).

Field, Input, Textarea, Select, Checkbox, Switch, Radio Group, Segmented Control und der OTP Input sind kostenlos. Phone Input, Date und Time Picker, Combobox, Multi-select, Number und Currency Input und die form-validation-Lib gehören zu Pro. Pro kostet Stand Oktober 2026 $8 im Monat, $80 im Jahr oder einmalig $150 (siehe [Preise für Wingo UI Pro](https://wingo-ui.com/de/pricing)).

## Welche Komponenten-Guides gehen tiefer?

Diese Artikel aus der [Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components) führen ein Feld, eine Regel oder eine Komponente weiter:

- [React OTP Input](https://wingo-ui.com/de/blog/react-otp-input): Einfügen, SMS-Autofill, react-hook-form mit zod und ein Resend-Timer.
- [Rumänische CUI-Validierung](https://wingo-ui.com/de/blog/romanian-cui-validation-javascript): die Prüfziffer der Steuernummer einer Firma, eine zod-Regel und eine Abfrage, die beweist, dass die Firma existiert.
- [IBAN-Validierung in JavaScript](https://wingo-ui.com/de/blog/iban-validation-javascript): die Mod-97-Prüfung, warum ein Regex nicht reicht, und ein Feld, das beim Tippen formatiert.
- [Rumänischer CNP-Validator](https://wingo-ui.com/de/blog/romanian-cnp-validator-javascript): eine persönliche Identifikationsnummer dekodieren und validieren, und die Frage, ob du sie überhaupt speichern solltest.
- [Data Table mit TanStack Table v9](https://wingo-ui.com/de/blog/tanstack-table-v9-data-table): Filter, Massenaktionen, Pinning und Server-Pagination, mit Karten auf dem Smartphone.
- [React Command Palette](https://wingo-ui.com/de/blog/react-command-palette): Cmd+K mit verschachtelten Seiten, asynchronen Ergebnissen und einem Sheet in voller Höhe auf dem Smartphone.
- [dnd kit auf dem Smartphone](https://wingo-ui.com/de/blog/dnd-kit-mobile-touch-drag): Touch-Drag, bei dem die Seite trotzdem scrollt, und ein Weg per Tap für Leute, die nicht ziehen können.
- [shadcn-Komponenten aktualisieren](https://wingo-ui.com/de/blog/update-shadcn-components-without-losing-edits): Fixes von upstream in Dateien holen, die du schon bearbeitet hast.
- [React Phone Input](https://wingo-ui.com/de/blog/react-phone-input): Formatierung pro Land beim Tippen, ein Land aus einer eingefügten +Vorwahl und ein E.164-Wert.
- [React Date Range Picker](https://wingo-ui.com/de/blog/react-date-range-picker): Presets, zwei Monate mit Apply und Cancel auf dem Desktop und ein Bottom Sheet auf dem Smartphone.
- [React Tag Input](https://wingo-ui.com/de/blog/react-tag-input): Chips aus Tippen und Einfügen, Vorschläge, Limits und Duplikate, und wann ein Multi-select besser passt.
- [React Multi Step Form](https://wingo-ui.com/de/blog/react-multi-step-form): zod-Validierung pro Schritt, eine asynchrone Prüfung und Fortschritt, der ein Neuladen übersteht.
- [React Stepper Component](https://wingo-ui.com/de/blog/react-stepper-component): Zustände und Varianten von Schritten für Checkout, Onboarding und KYC, und wie ein Stepper auf dem Smartphone zusammenklappt.
- [Spring-Presets für Framer Motion](https://wingo-ui.com/de/blog/framer-motion-spring-presets): vier Springs mit echten Werten für Stiffness und Damping und die Controls, zu denen jede passt.
- [shadcn-Registry für Komponenten von Drittanbietern](https://wingo-ui.com/de/blog/shadcn-registry-third-party-components): Komponenten einer anderen Bibliothek in ein shadcn/ui-Projekt installieren und prüfen, was dabei geschrieben wird.
- [React Combobox](https://wingo-ui.com/de/blog/react-combobox): asynchrone Suche ohne Race Conditions, freier Text, und wann ein Select oder eine Listbox besser passt.
- [React Multi Select](https://wingo-ui.com/de/blog/react-multi-select): Zeilen mit Checkbox, Chips, die zu +N zusammenfallen, Suche, Alle auswählen, ein Maximum und ein Bottom Sheet auf dem Smartphone.
- [React Currency Input](https://wingo-ui.com/de/blog/react-currency-input): Gruppierung mit Intl.NumberFormat beim Tippen, ein Cursor, der nicht springt, und eine einfache Zahl für deinen Code.
- [React Color Picker](https://wingo-ui.com/de/blog/react-color-picker): ein Sättigungsfeld und ein Farbtonbalken, Felder für HEX, RGB und HSL, die EyeDropper API und OKLCH, abgeleitet in CSS.
- [React File Upload](https://wingo-ui.com/de/blog/react-file-upload): Drag and Drop, Einfügen und die Kamera des Smartphones, Validierung vor dem Upload, Fortschritt und Abbrechen.
- [React Signature Pad](https://wingo-ui.com/de/blog/react-signature-pad): weiche Tinte auf einem Canvas, die eine Drehung übersteht, ein Modus mit getipptem Namen und Export als PNG oder SVG.
- [React CSV Importer](https://wingo-ui.com/de/blog/react-csv-importer): hochladen, Spalten zuordnen, die Zeilen korrigieren, die bei der Validierung scheitern, und mit Bericht importieren.

Für Daten, die nicht in ein Formular passen, behandeln diese Guides die Viewer, Tabellen und Planungsboards derselben Kategorie:

- [React Virtualized Table](https://wingo-ui.com/de/blog/react-virtualized-table): 100.000 Zeilen mit TanStack Virtual, gerendert werden nur die sichtbaren.
- [React JSON Viewer](https://wingo-ui.com/de/blog/react-json-viewer): ein ausklappbarer Baum mit einer Suche, die passende Zweige öffnet, und kopierbaren Pfaden.
- [React Diff Viewer](https://wingo-ui.com/de/blog/react-diff-viewer): Split- und Unified-Diffs mit Änderungen auf Wortebene unter React 19.
- [React Log Viewer](https://wingo-ui.com/de/blog/react-log-viewer): ein Live-Stream aus 100.000 Zeilen mit Filtern nach Level und Regex und einem Follow-Modus.
- [React DOCX Viewer](https://wingo-ui.com/de/blog/react-docx-viewer): Word-Dateien als echte Seiten gesetzt, mit Kommentaren und Änderungsverfolgung, im Browser geparst.
- [React Excel Viewer](https://wingo-ui.com/de/blog/react-excel-viewer): .xlsx-Dateien mit ihren echten Styles, Zahlenformaten, fixierten Bereichen und Tabellenblättern.
- [React Kanban Board](https://wingo-ui.com/de/blog/react-kanban-board): Ziehen mit der Maus, per Long Press oder mit der Tastatur, WIP-Limits und eine Spalte nach der anderen auf dem Smartphone.
- [React Resource Scheduler](https://wingo-ui.com/de/blog/react-resource-scheduler): Räume, Autos oder Leute per Ziehen auf einer Zeitleiste buchen, die Überschneidungen ablehnt.

Weitere Picker und Eingaben aus derselben Kategorie:

- [React Time Picker](https://wingo-ui.com/de/blog/react-time-picker): ein Feld, in das man tippen kann, 12 oder 24 Stunden je nach Locale, gebuchte Slots gesperrt und Räder auf dem Smartphone.
- [React Month Picker](https://wingo-ui.com/de/blog/react-month-picker): ein Monat, ein Quartal oder ein Zeitraum aus einem Jahresraster, mit Presets, Grenzen und einem Bottom Sheet auf dem Smartphone.
- [React Mentions](https://wingo-ui.com/de/blog/react-mentions): @Personen und #Tags als Markup mit ID gespeichert, das Umbenennungen übersteht, mit Vorschlägen am Cursor.
- [React Rich Text Editor](https://wingo-ui.com/de/blog/react-rich-text-editor): Tiptap mit Markdown-Kürzeln, @Mentions und Bild-Uploads, gespeichert als HTML, JSON oder Markdown.
- [React Rating Stars](https://wingo-ui.com/de/blog/react-rating-stars): halbe Werte, eine Vorschau beim Hover, Tastatur und Touch und Durchschnittswerte nur zum Lesen.
- [React Cron Generator](https://wingo-ui.com/de/blog/react-cron-generator): Tabs für die Häufigkeit und Chips für Wochentage, der Zeitplan als Satz und die nächsten Läufe als Liste.
- [React Location Picker](https://wingo-ui.com/de/blog/react-location-picker): ein ziehbarer Pin auf einer echten Karte, ein Radius per Slider, Adresssuche und eine statische Karte, wenn die Karte ausfällt.
- [React Image Cropper](https://wingo-ui.com/de/blog/react-image-cropper): Zuschnitt vor dem Upload mit Pinch-Zoom und Drehung, Export als WebP in fester Größe und ein runder Avatar-Upload.

Für Charts, Medien, Kalender und Feedback in der App:

- [React Charts mit Tailwind](https://wingo-ui.com/de/blog/react-charts-tailwind): Area-, Bar-, Line- und Donut-Charts plus ein Chart-Kit auf d3-scale und d3-shape.
- [React Sparkline](https://wingo-ui.com/de/blog/react-sparkline): Tabellenzellen und KPI-Kacheln auf einer gemeinsamen Skala, mit Delta-Chip und Scrubbing per Tastatur.
- [React Gauge Chart](https://wingo-ui.com/de/blog/react-gauge-chart): ein Wert vor Zonen und einem Zielwert, lesbar bei 120px und als Meter vorgelesen.
- [React GitHub Contribution Graph](https://wingo-ui.com/de/blog/react-github-contribution-graph): ein Graph aus einem Benutzernamen oder deinen eigenen Daten und eine Aktivitäts-Heatmap mit Tastaturzugriff.
- [React Gantt Chart](https://wingo-ui.com/de/blog/react-gantt-chart): ziehbare Balken, Griffe für den Fortschritt und Pfeile für Abhängigkeiten, auf einem Server gespeichert.
- [React Kalender mit Terminen](https://wingo-ui.com/de/blog/react-calendar-component-with-events): Monats-, Wochen-, Tages- und Agenda-Ansicht, Verschieben per Ziehen und Speichern mit Rollback.
- [React Tree View](https://wingo-ui.com/de/blog/react-tree-view): Checkboxen mit drei Zuständen, Kinder per Lazy Loading, ein Filter, der Eltern behält, und Verschieben per Ziehen.
- [React Animated Counter](https://wingo-ui.com/de/blog/react-animated-number-counter): Ziffern wie ein Kilometerzähler, Trennzeichen aus Intl, eine feste Breite und Rücksicht auf reduzierte Bewegung.
- [React Countdown Timer](https://wingo-ui.com/de/blog/react-countdown-timer): Ziffernblöcke, Inline-Text oder Wörter, mit Zeitzonen, schlafenden Tabs und Hydration im Griff.
- [Embla Carousel in React](https://wingo-ui.com/de/blog/embla-carousel-react): Slides für Fotos, Angebote, Testimonials und Onboarding mit Tastatursteuerung und pausierbarem Autoplay.
- [React Image Lightbox](https://wingo-ui.com/de/blog/react-image-lightbox): wächst aus dem Thumbnail, wechselt per Wischen zwischen Fotos, zoomt per Pinch und schließt mit einem Wischen nach unten.
- [React Audio Player mit Waveform](https://wingo-ui.com/de/blog/react-audio-player-with-waveform): Scrubbing über die Waveform für Podcasts und Sprachnachrichten, Spulen per Tastatur und vorberechnete Peaks.
- [React PDF Viewer](https://wingo-ui.com/de/blog/react-pdf-viewer): Suche, Thumbnails und Highlights, die als echte PDF-Annotationen exportiert werden.
- [React QR Code mit Logo](https://wingo-ui.com/de/blog/react-qr-code-with-logo): runde oder gepunktete Module, eine freie Kachel in der Mitte, die richtige Fehlerkorrektur und Export als PNG oder SVG.
- [React Empty State](https://wingo-ui.com/de/blog/react-empty-state-component): erste Nutzung, keine Treffer und Fehler, jeweils mit einem Text, der erklärt, und einer Aktion.
- [React Skeleton Loader](https://wingo-ui.com/de/blog/react-skeleton-loader): Platzhalter in der Form des echten Inhalts, und wann ein Spinner besser passt.
- [React Notification Center](https://wingo-ui.com/de/blog/react-notification-center): eine Glocke für Ungelesenes, Filter-Tabs, Gruppen nach Tag und Wischen zum Entfernen, und was stattdessen in einen Toast gehört.
- [React Product Tour](https://wingo-ui.com/de/blog/react-product-tour): ein Spotlight auf die echte UI, ein Dock unten auf dem Smartphone und wann ein Hotspot besser passt.

## Womit solltest du anfangen?

Installier die kostenlose Anatomie und die Controls, die die meisten Formulare brauchen, und bau dann ein echtes Formular damit:

```bash
npx wingo-ui@latest add field input textarea select checkbox
```

Der Befehl kopiert die Quelldateien in dein Projekt, installiert die npm-Pakete, die sie importieren, und hält die Versionen fest, damit spätere Updates sich mit deinen Änderungen zusammenführen lassen. In einem Projekt ohne `wingo-ui.json` führst du zuerst `npx wingo-ui@latest init` aus. Öffne die [Doku zu Field](https://wingo-ui.com/components/field) für den Playground und jede Prop, und ergänze die Teile für Telefon, Datum und Validierung, wenn deine Formulare sie brauchen.

## Komponenten in diesem Artikel

- [Field](https://wingo-ui.com/components/field) (Free): A React form field wrapper every control shares: label, hint, animated error and counter, fieldsets, and the box recipe all inputs use. Installation: `npx wingo-ui@latest add field`
- [Input](https://wingo-ui.com/components/input) (Free): A React text input: icons and addons inside the box, a clear button, a loading spinner, a character counter and floating labels. Installation: `npx wingo-ui@latest add input`
- [Select](https://wingo-ui.com/components/select) (Free): A React select with search, groups, icons, virtualized rows and a hidden native select for forms. Installation: `npx wingo-ui@latest add select`
- [OTP Input](https://wingo-ui.com/components/otp-input) (Free): A React OTP input for one-time verification codes and PINs, with digits that roll into place and a caret that slides between slots. Installation: `npx wingo-ui@latest add otp-input`
- [Phone Input](https://wingo-ui.com/components/phone-input) (Pro): A React phone number input with a searchable country picker, formatting as you type, validation and one clean E.164 value out. Installation: `npx wingo-ui@latest add phone-input`
- [Date Picker](https://wingo-ui.com/components/date-picker) (Pro): A React date picker: type a date in your locale or pick it from a calendar, with presets, limits, optional time and a phone sheet. Installation: `npx wingo-ui@latest add date-picker`
- [Form validation](https://wingo-ui.com/components/form-validation) (Pro): Zod validation rules for React forms with one message set (English or Romanian), react-hook-form glue and a pretend request. Installation: `npx wingo-ui@latest add form-validation`
- [Textarea](https://wingo-ui.com/components/textarea) (Free): An auto-growing React textarea with a character counter, an optional resize handle, a toolbar slot and mod+enter to submit. Installation: `npx wingo-ui@latest add textarea`
- [Checkbox](https://wingo-ui.com/components/checkbox) (Free): A React checkbox that is on, off or mixed, with a check that draws itself, full-row labels, cards, and groups with select all. Installation: `npx wingo-ui@latest add checkbox`

## Häufige Fragen

### Brauche ich react-hook-form, um Formulare in React zu bauen?

Nein. Ein kurzes Formular funktioniert mit nativen Inputs, name-Attributen und FormData, und Input, Textarea, Select, Checkbox, Phone Input und Date Picker von Wingo UI senden ihren Wert über ein natives oder verstecktes Input, sobald sie einen name haben. React Hook Form lohnt sich, sobald du das Timing der Validierung pro Feld, Regeln über mehrere Felder, Dirty State oder Field Arrays brauchst.

### Wie nutze ich react-hook-form mit eigenen Komponenten?

Nimm register, wenn die Komponente ein natives Input rendert und onChange, onBlur und ref daran weitergibt, etwa bei einem Textfeld. Nimm Controller für alles, dessen Wert kein String in einem nativen Input ist (ein Select, ein Datum, eine Telefonnummer, eine Checkbox), und gib field.ref weiter, damit ein gescheitertes Absenden das Feld fokussieren kann.

### Wie mache ich Formularfelder in React barrierefrei?

Verknüpf jedes Label über htmlFor und id mit seinem Control, lass aria-describedby auf den Hinweis zeigen oder, solange er sichtbar ist, auf den Fehler, und setz aria-invalid auf das ungültige Control. Beschreib jeden Fehler in Text statt nur mit Farbe, kündige ihn über eine Live-Region mit „polite“ an und setz den Fokus auf das erste ungültige Feld, wenn das Absenden scheitert.

### Warum zoomt mein iPhone hinein, wenn ich auf ein Input tippe?

Safari auf iOS zoomt die Seite, wenn der Text im fokussierten Feld kleiner als 16px ist. Gib Inputs auf dem Smartphone 16px Text und geh erst ab einem breiteren Breakpoint kleiner, statt Pinch-Zoom im Viewport-Meta-Tag abzuschalten.

### Soll ich ein Select, eine Radio Group oder eine Combobox nehmen?

Nimm eine Radio Group oder ein Segmented Control für zwei bis fünf Optionen, die man auf einen Blick vergleichen soll, ein Select für eine bekannte Liste mit bis zu ein paar hundert Optionen und eine Combobox, wenn die Liste riesig ist, von einem Server kommt oder freien Text erlaubt.

### Welche Formular-Komponenten von Wingo UI sind kostenlos?

Field, Input, Textarea, Select, Checkbox, Switch, Radio Group, Segmented Control, Search Input, Listbox und der OTP Input sind kostenlos. Phone Input, Date und Time Picker, Combobox, Multi-select, die Inputs für Zahlen, Währung, Passwort und E-Mail, File Upload und die form-validation-Lib gehören zu Pro.

---

Quelle: https://wingo-ui.com/de/blog/react-form-components-guide
