React OTP Input: Einfügen, SMS-Autofill und Resend-Timer

Ein Screen für den Bestätigungscode sieht nach einem Nachmittag Arbeit aus: sechs Felder, der Fokus springt weiter, fertig. Die Fehler tauchen später auf, auf echten Smartphones. Sechs Eingabefelder mit je einem Zeichen machen aus dem Code, den das iPhone über der Tastatur anbietet, oder aus einem eingefügten Code eine einzelne Ziffer. Backspace in einem leeren Feld tut nichts, und der Link zum erneuten Senden feuert so schnell, wie jemand tippen kann. Dieser Guide zeigt, was ein React OTP Input können muss, warum SMS-Autofill ohne Fehlermeldung scheitert, und baut ein komplettes Bestätigungsformular mit react-hook-form, zod und einem Resend-Timer. Die Beispiele nutzen den kostenlosen OTP Input von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral; Fakten von außen verlinken auf ihre Quellen.
Was muss ein React OTP Input können?
Er muss einen Code auf vier Wegen annehmen (Tippen, Einfügen, den Vorschlag der Tastatur aus einer SMS und auf Android die WebOTP API) und sich trotzdem wie ein einziges Feld bearbeiten lassen. Unsere Checkliste, und ein fairer Test für jede Bibliothek:
- Tippen springt ins nächste Feld, die Pfeiltasten wechseln zwischen den Feldern, und ein Tipp hinter das erste leere Feld landet in genau diesem Feld. So überspringt das Tippen nie eins.
- Einfügen füllt alle Felder auf einmal und behält nur die Zeichen, die sie annehmen. „123 456“ und „Dein Code lautet 123456“ ergeben also beide sechs Ziffern.
- SMS-Autofill braucht
autocomplete="one-time-code"auf dem Feld, das den Code empfängt, und dort kein Limit auf ein Zeichen. - Backspace leert das Feld, in dem du gerade bist. Der nächste Druck springt zurück und leert das Feld davor.
- Die richtige Tastatur:
inputMode="numeric"für Ziffern, eine volle Tastatur mit Großbuchstaben fürtype="letters"oder"both". - Feedback:
status="error"färbt die Felder rot und schüttelt die Reihe einmal,"success"zieht einen grünen Ring um jedes Feld, und reduzierte Bewegung schaltet die Animation ab. - Namen: Jedes Feld wird als „Digit 1 of 6“ vorgelesen (
slotLabelübersetzt das), die Reihe braucht ein Gruppen-Label, und der Fehler braucht Worte neben dem roten Ring.
Installier ihn und setz ihn in eine Client-Komponente:
length legt die Zahl der Felder fest (4 für eine PIN), type die erlaubten Zeichen, mask verbirgt sie wie ein Passwortfeld, und size wählt Felder mit 40, 48 oder 56px. Die Größe sm wächst auf Touchscreens auf 44px, und in einem Container, der schmaler als die Reihe ist, schrumpfen die Felder gleichmäßig und bleiben quadratisch. So passen auch acht Felder auf ein 360px breites Smartphone. Mehr zu Größen für den Daumen steht in Mobile-first React Components.
$ npx wingo-ui@latest add otp-inputWie funktioniert Autofill mit autocomplete one-time-code?
Das Attribut autocomplete="one-time-code" ist ein Hinweis an den Browser. Das Betriebssystem erkennt einen Code in einer eingehenden SMS, und wenn das fokussierte Feld dieses Token trägt, bietet die Tastatur den Code an. Ein Tipp fügt den ganzen Code auf einmal in dieses Feld ein. MDN definiert das Token (opens in a new tab) als Einmalpasswort (OTP), das die Identität bestätigt und bei der Anmeldung als zusätzlicher Faktor dient.
Ob autocomplete one-time-code greift, hängt von der Plattform ab. Laut dem Guide von web.dev zu SMS-OTP-Formularen (opens in a new tab) (zuletzt im Dezember 2020 aktualisiert und bei unserer Prüfung im Oktober 2026 noch die aktuelle Fassung) funktioniert das Attribut in Safari ab Version 12 auf iOS, iPadOS und macOS. Chrome, Opera und Vivaldi auf Android unterstützen an den Origin gebundene Codes dagegen über die WebOTP API und ausdrücklich nicht über autocomplete="one-time-code".
Seit iOS 14 und macOS Big Sur kannst du einen Code bei Apple auch an deine Domain binden. Laut Apples Doku zu domaingebundenen Codes (opens in a new tab) steht in der letzten Zeile der Nachricht deine Domain nach einem @ und der Code nach einem #, getrennt durch ein Leerzeichen. AutoFill schlägt einen domaingebundenen Code nur vor, wenn die in Safari geöffnete Seite zu dieser Domain passt:
Die WebOTP API von Chrome liest dieselbe letzte Zeile, also bedient ein Nachrichtenformat beide Plattformen.
Der OTP Input setzt das Token auf sein erstes Feld und alle anderen auf off. Seine Felder haben kein maxLength, deshalb kommt ein eingefügter Code in einem einzigen Change-Event an. Die Komponente behandelt ihn wie einen eingefügten Text und verteilt ihn auf die Felder. Ein Tipp auf eine leere Reihe landet immer im ersten Feld, so beginnt ein automatisch ausgefüllter Code an der richtigen Stelle.
Warum scheitert React OTP Input Autofill ohne Fehlermeldung?
Autofill scheitert still, weil eine verfehlte Bedingung kein Fehler ist: Die Tastatur zeigt keinen Vorschlag, und in der Konsole steht nichts. Die Ursache liegt im Markup, in der Nachricht oder im Testaufbau. Das prüfen wir, in dieser Reihenfolge:
- Jedes Feld hat
maxLength={1}. Das Limit kürzt alles, was jemand einfügt, auch den Tipp auf den vorgeschlagenen Code. Nur die erste Ziffer kommt an. Entferne das Limit und filtere im Change-Handler. - Das Token fehlt oder wird überschrieben. Ein Wrapper, der
autoCompletenicht weiterreicht, oder einautocomplete="off", das jemand gegen ein anderes Autofill eingebaut hat. Laut MDN darf der Browser beiofffür dieses Feld keinen Wert automatisch eintragen oder auswählen. - Die Nachricht ist an eine andere Domain gebunden. Ein Code, der auf
@example.com #123456endet, wird auf einem Preview-Deployment oder einem Staging-Host unter einer anderen Domain nicht angeboten. Mach die Domain in der Nachricht zu einer Einstellung pro Umgebung. - Du testest auf Android nur mit dem Attribut. Chrome dokumentiert dafür die WebOTP API; mehr dazu im nächsten Abschnitt.
- Der Listener startet zu spät, oder die Seite läuft über HTTP. Laut Chromes WebOTP-Guide (opens in a new tab) muss die Nachricht gesendet werden, nachdem
navigator.credentials.get()aufgerufen wurde. Zeig den Code-Schritt also, sobald die Sendeanfrage startet, nicht erst nach einer Weiterleitung. Die API gibt es außerdem nur in sicheren Kontexten. Wenn du deinen Dev-Server vom Smartphone unterhttp://192.168.1.20:3000öffnest, ist die Feature-Prüfung false. Nutze einen HTTPS-Tunnel. - Der Code passt nicht in die Felder. Ein Code mit 8 Zeichen in einem Input mit 6 Feldern verliert seine letzten zwei Zeichen, und ein Code mit Buchstaben verliert die Buchstaben, wenn
typeauf"numbers"steht. Stimmlengthundtypeauf das ab, was dein Server sendet. - Du hast im Emulator oder in einem Headless-Browser getestet. Das README von input-otp (opens in a new tab) weist darauf hin, dass sich SMS-Autofill nicht headless testen lässt. Teste auf einem echten Smartphone mit einer echten Nachricht.
Wie lese ich den Code auf Android mit der WebOTP API?
Ruf navigator.credentials.get({ otp: { transport: ["sms"] } }) auf, sobald der Code-Screen gemountet wird, brich den Aufruf beim Unmount ab und schreib den Code in den Wert des Inputs. Kommt eine passende Nachricht an, bittet Chrome die Person in einem Bottom Sheet um Bestätigung und löst das Promise dann mit dem Code auf. Prüf vorher, ob es die API gibt: MDN kennzeichnet sie als experimentell und nicht Baseline, und Safari setzt stattdessen auf das Attribut. Speicher das als hooks/use-web-otp.ts:
Der Cast ist nötig, weil den DOM-Typen von TypeScript die Option otp fehlt, und useEffectEvent braucht React 19.2, das der App Router von Next.js 16 nutzt. Jeder Aufruf von get() löst nur einmal auf, deshalb startet attempt den Listener nach einem erneuten Senden neu. In einem Cross-Origin-iframe braucht die API außerdem die Berechtigung otp-credentials, so steht es auf der WebOTP-Seite von MDN (opens in a new tab).
Wie validiere ich einen OTP Input mit React Hook Form und zod?
Pack ihn in Controller und validiere den zusammengesetzten String mit zod. Ein React Hook Form OTP Input braucht Controller, weil register native Inputs über eine ref liest und zurücksetzt, der OTP Input aber eine Reihe von Inputs mit einem gemeinsamen String-Wert ist. Für Label, Hinweis und Fehler setzt du ihn in ein Field mit labelAs="span" und lässt FieldControl die IDs an die Reihe weitergeben: Die Gruppe bekommt aria-labelledby und aria-describedby, und ein Klick auf das Label fokussiert das erste Feld.
Der komplette Bestätigungsschritt nutzt den WebOTP-Hook von oben, einen Resend-Timer und einen Button für jede Aktion. Installier, was er importiert (useCooldown ist ein Pro-Item):
Die Komponente bekommt verify und resend als Props, so kann eine Next.js-Seite zwei Server Actions übergeben:
Drei Details, die du übernehmen solltest:
reValidateMode: "onSubmit"verhindert, dass „Gib den 6-stelligen Code ein.“ beim erneuten Tippen aufblitzt, undclearErrorsinonChangeblendet die alte Meldung beim ersten Tastendruck aus.- Ein falscher Code leert die Felder und fokussiert das erste.
field.refwürde auf demdivlanden, das die Reihe umschließt und keinen Fokus annehmen kann. Deshalb sucht das Beispiel das erste Feld über die ID des Field. - Ohne React Hook Form, in einem Formular, das an eine Server Action sendet, renderst du
<input type="hidden" name="code" value={code} />neben der Reihe. Die Felder haben keinname, also würdeFormDatasie nicht sehen.
Wie baue ich einen Resend-Timer in einen OTP-Screen?
Sperr den Button zum erneuten Senden nach jedem Versand für eine feste Wartezeit, und speicher, wann die Wartezeit endet, statt eine Zahl herunterzuzählen. Ein Zähler mit setInterval driftet, wenn das Smartphone sperrt oder der Tab schläft, und ein Neuladen gibt einen frischen Button heraus. Der Hook useCooldown merkt sich den Endzeitpunkt, tickt einmal pro Sekunde passend dazu, übersteht mit persistKey ein Neuladen und öffnet den Screen mit autoStart schon in der Wartezeit, weil der erste Code ja gerade gesendet wurde.
$ npx wingo-ui@latest add use-cooldownDer Hook liefert remaining, active, progress für einen Ring und ein label wie „0:42“ für den Button. Der Button schützt nur ehrliche Nutzer: Jeder kann deinen Sende-Endpunkt direkt aufrufen. Setz deshalb dieselbe Wartezeit und ein Tageslimit auf dem Server durch, pro Telefonnummer und IP-Adresse. useCooldown gehört zu Wingo UI Pro. Wenn du deinen eigenen Hook schreibst, halte den End-Zeitstempel im State und berechne die restlichen Sekunden bei jedem Tick aus Date.now().
Solltest du stattdessen den shadcn OTP Input nutzen?
Nimm den shadcn OTP Input (opens in a new tab), wenn du schon shadcn/ui nutzt, ein Trennzeichen zwischen zwei Dreiergruppen willst oder den Code in einem echten Input haben möchtest. Nimm den OTP Input von Wingo UI für eingebaute Erfolgs- und Fehlerzustände, Größen für Touch und ein einziges Element, das du über Props konfigurierst. Die shadcn-Version basiert auf input-otp: Es rendert einen Text-Input, macht ihn unsichtbar und zeichnet die Felder aus seinem State. Unsere Komponente rendert einen Input pro Feld. Beide verarbeiten Einfügen und SMS-Autofill. Stand Oktober 2026:
Das README von input-otp (opens in a new tab) zählt auch auf, was ein einzelner Input von einem nativen Textfeld behält: Rückgängig, Alles auswählen, teilweises Einfügen in die Mitte eines halb ausgefüllten Codes und Platz für die Badges von Passwortmanagern. Unsere Felder verarbeiten Einfügen, Backspace und die Pfeiltasten im Code, aber Rückgängig und Alles auswählen enden an einem einzelnen Feld.
Wie füge ich den OTP Input in ein Next.js-Projekt ein?
Führ den Add-Befehl in einem Next.js-Projekt mit Tailwind CSS v4 aus. Die CLI kopiert die Quelldateien in dein Projekt und installiert die npm-Pakete, die sie importieren:
Die Doku zum OTP Input enthält den Playground und alle Props. Wenn du lieber mit einem fertigen Screen startest: Der Block Login Page bringt diesen Ablauf in einer Anmeldeseite mit, mit einer Code-Ansicht, einem Resend-Timer und einer Sperre von 60 Sekunden nach drei falschen Codes. Login Page und useCooldown gehören zu Wingo UI Pro. Pro kostet Stand Oktober 2026 $8 im Monat, $80 im Jahr oder einmalig $150 (Preise von Wingo UI). Für den Rest eines Formulars lies den Guide zu React-Formularkomponenten; weitere Guides findest du in der Kategorie Komponenten-Guides.
Häufige Fragen
Wie fülle ich in React einen OTP-Code aus der SMS automatisch aus?
Setz autocomplete="one-time-code" auf das Eingabefeld, das den Code empfängt, verzichte auf maxLength={1} und beende die SMS mit einer Zeile wie „@example.com #123456“. Ruf auf Android zusätzlich die WebOTP API auf, denn Chrome dokumentiert an den Origin gebundene Codes über diese API statt über das Attribut.
Warum füllt OTP-Autofill nur das erste Feld?
Jedes Feld hat ein maxLength von 1, also kürzt der Browser den eingefügten Code auf ein Zeichen, bevor dein Change-Handler läuft. Entferne das Limit, filtere die Zeichen selbst und verteile eine Änderung mit mehreren Zeichen auf die Felder, genau wie beim Einfügen.
Funktioniert autocomplete one-time-code in Chrome auf Android?
Verlass dich nicht darauf. web.dev nennt das Attribut für Safari auf iOS, iPadOS und macOS und führt Chrome, Opera und Vivaldi auf Android stattdessen unter der WebOTP API. Behalte das Attribut für Safari und ergänze für Android einen Aufruf von navigator.credentials.get mit dem Transport sms.
Wie nutze ich einen OTP Input mit react-hook-form?
Pack ihn in Controller, gib field.value und field.onChange weiter und ruf handleSubmit in onComplete auf, damit die letzte Ziffer das Formular absendet. Validiere mit einer zod-Regel wie einem Regex für sechs Ziffern und entferne den Fehler, sobald die Person wieder tippt.
Ist der OTP Input von Wingo UI kostenlos?
Ja. OTP Input, Field und Button sind kostenlos und lassen sich mit npx wingo-ui@latest add otp-input field button installieren. Der Hook useCooldown und der Block Login Page gehören zu Wingo UI Pro.
- OTP
- Authentifizierung
- React Hook Form
- Mobile UI
- Barrierefreiheit


