Shadcn Login Page: Magic Link, Codes, 2FA und Reset

Eine shadcn login page fängt meist als Block an, den du an einem Nachmittag kopierst, und endet als Woche voller Sonderfälle. Das Formular schickt ab, dann vertippt sich jemand zweimal beim Passwort, fordert einen Reset-Link an, öffnet ihn auf dem Handy oder meldet sich mit Google an und sieht einen Spinner, der sich endlos dreht. shadcn/ui liefert fünf Login-Blöcke (login-01 bis login-05), aber das sind Layouts. Das Formular in login-03 hat zum Beispiel kein onSubmit, keine Fehlermeldung und einen Link „Forgot your password?“, der auf # zeigt (geprüft am 16. Oktober 2026 in den Login-Blöcken von shadcn/ui (opens in a new tab)). Galerien wie shadcnblocks (opens in a new tab) und Tailark (opens in a new tab) bieten weitere Layouts, und die Anbindung an dein Auth-Backend bleibt trotzdem deine Aufgabe.
In diesem Tutorial baust du eine komplette Login-Seite für Next.js mit dem Block Login Page von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Es geht um Anmeldung mit Passwort, Magic Links, E-Mail-Codes, Social Login, Zwei-Faktor-Authentifizierung und Passwort-Wiederherstellung, dazu die Fehlerzustände, die die meisten Vorlagen weglassen.
Was braucht eine Login-Seite für die Produktion?
Acht Screens, auch wenn eine Person meist nur zwei davon zu sehen bekommt:
- Anmelden. E-Mail, Passwort, „Angemeldet bleiben“, ein Link für ein vergessenes Passwort, Social Buttons und ein Wechsel zu einer passwortlosen Methode.
- Schau in dein Postfach. Nach einem Magic Link: die Adresse, ein Button zum erneuten Senden, der wartet, und ein Weg zu einer anderen Adresse.
- Code eingeben. Ein 6-stelliger Code per E-Mail für die passwortlose Anmeldung.
- Zwei-Faktor-Bestätigung. Der Code aus der Authenticator-App nach einem richtigen Passwort, mit Backup-Code als Ausweg.
- Passwort vergessen. Ein E-Mail-Feld.
- Reset-Link gesendet. Eine Meldung, die nie bestätigt, ob das Konto existiert.
- Neues Passwort festlegen. Regeln für die Passwortstärke und ein Feld zur Bestätigung.
- Passwort geändert. Ein klarer Weg zurück zur Anmeldung.
Der Block Login Page rendert alle acht als Ansichten einer einzigen Komponente (sign-in, magic-link-sent, otp, two-factor, forgot, forgot-sent, reset, reset-done). Jede Ansicht gleitet von der Seite herein, zu der sie gehört, der Fokus springt auf ihren Titel, und Screenreader sagen den Wechsel an. Auf Smartphones füllt das Formular den Bildschirm mit 48px hohen Feldern, und der Hero-Bereich fällt weg.
$ npx wingo-ui@latest add login-pageWie installierst du den Login-Page-Block?
Ein CLI-Befehl kopiert den Block und alles, was er importiert, in dein Projekt:
Das CLI kopiert components/blocks/login-page.tsx, seinen Layout-Rahmen und seine Beispieldaten als Quellcode in dein Projekt, dazu die Teile, die er importiert: Password Input für den Umschalter zum Einblenden des Passworts und die Warnung bei aktiver Feststelltaste, OTP Input für die Code-Felder, Input, Button, Alert, Checkbox, Toast, den Hook use-cooldown und die Helfer für die Formularvalidierung. Außerdem installiert es motion, lucide-react, react-hook-form und zod. Die Datei beginnt mit "use client". Eine Server Component kann sie rendern, aber die Props, die du in der Produktion brauchst (onSuccess mit dem Router, linkComponent={Link}, Handler, die zurückgegebene Fehler in Ablehnungen verwandeln), sind Funktionen, die im Browser definiert werden. In der Praxis packst du den Block deshalb in eine kleine Client-Komponente.
Anders als eine statische Tailwind Login Page hält der Block Zustand, Validierung und Ansichtswechsel in der Datei selbst, und jede Klasse nutzt die Theme-Tokens (bg-background, text-muted-foreground, border-border). So passt er sich ohne Änderungen an dein helles und dunkles Theme an. Weil er Quellcode ist, kannst du trotzdem jede Zeile anpassen.
Wie verbindest du den Block mit Next.js Server Actions?
Jeder Handler (onSignIn, onMagicLink, onSendCode, onVerifyCode, onForgot, onReset, onProvider) darf ein Promise zurückgeben. Solange es aussteht, zeigt der Button einen Spinner. Wird es abgelehnt, landet die Meldung des Fehlers im roten Alert über dem Formular. Wird es erfüllt, wechselt der Block zur nächsten Ansicht oder ruft onSuccess auf.
Der Authentifizierungs-Guide von Next.js (opens in a new tab) nutzt Server Actions für die Anmeldelogik, weil sie immer auf dem Server laufen. Der Guide zur Fehlerbehandlung (opens in a new tab) rät, erwartbare Fehler wie ein falsches Passwort als Rückgabewerte zu modellieren, statt sie zu werfen (beide geprüft am 16. Oktober 2026). Dazu kommt ein praktischer Grund: In einem Produktions-Build ersetzt Next.js die Meldung eines Fehlers, der vom Server weitergereicht wird, durch eine allgemeine Meldung plus eine digest-ID, damit keine sensiblen Details nach außen dringen (Referenz zu error.js (opens in a new tab)). Ein „Falscher Code“, der in der Action geworfen wird, käme im Alert nur als Rauschen an. Die Action gibt ihren Fehler also als Daten zurück, und der Client macht daraus eine Ablehnung für den Block.
Die Server Actions
Das Objekt auth steht für die Bibliothek, die du nutzt. Dem Block ist egal, wie die Session entsteht. Ihn interessiert nur, ob das Promise erfüllt oder abgelehnt wird.
Der Client-Screen
Diese Server-Datei ist die ganze Route der Next.js Login Page, alles Interaktive steckt im Client-Screen. Drei Entscheidungen in diesem Code verdienen eine Erklärung.
Zwei-Faktor nur für Konten, die sie aktiviert haben. Die Prop twoFactor ist ein einfacher Boolean, der jede erfolgreiche Anmeldung mit Passwort zur Code-Ansicht schickt. Die meisten Apps wissen das aber erst nach der Passwortprüfung. Darum steuert der Screen view selbst und wechselt in onSuccess zu two-factor, wenn der Server das gemeldet hat. Ist der Code bestätigt, läuft onSuccess noch einmal, und der Router leitet weiter.
Die Reihenfolge der Methoden. methods={["password", "magic-link", "otp"]} zeigt zuerst das Passwortformular, darunter „Email me a sign-in link“ und „Email me a code“. Setzt du "otp" an den Anfang, startet die Seite als passwortloser Login. Mit methods={[]} und Providern bleiben nur die Provider-Buttons übrig. Das passt zu einer Firma, die Single Sign-on nutzt.
Echte Links. Mit linkComponent={Link} nutzen der Link „Create one“ und die Links im Footer next/link. „Create one“ führt zu registerHref, wo der Block Register Page die Registrierung im selben Layout übernimmt.
Welche Fehlerzustände muss eine Login-Seite abdecken?
Mindestens neun, vom falschen Passwort bis zum gesperrten Code-Feld. Genau diesen Teil lässt ein statischer shadcn login block weg. So reagiert die Login Page auf jeden Fehler, mit den englischen Standardtexten.
Zwei davon folgen direkt dem OWASP Authentication Cheat Sheet (opens in a new tab): Fehlgeschlagene Anmeldungen bekommen eine einzige allgemeine Meldung, die nie verrät, welcher Teil falsch war, und das Forgot Password Cheat Sheet (opens in a new tab) verlangt eine einheitliche Meldung für existierende und nicht existierende Konten. Halte dieses Versprechen auch auf dem Server: gleiche Antwort, ähnliches Timing.
Die Sperre für 60 Sekunden und die Wartezeit beim erneuten Senden sind reine Höflichkeit auf dem Client. Sie halten eine Person davon ab, auf den Button einzuhämmern, und helfen nicht gegen ein Skript, das deinen Endpoint aufruft. Setz auf dem Server ein Rate Limit pro Konto für Codes, Anmeldungen und Reset-Anfragen und schick die verbleibenden Versuche in der Fehlermeldung zurück, damit der Alert die Wahrheit sagt.
$ npx wingo-ui@latest add login-pageMagic Link oder Einmalcode: Was ist besser für passwortlosen Login?
Biete einen Code an, wenn Leute das Gerät wechseln oder hinter einem Mailfilter in der Firma sitzen, und einen Link, wenn sie sich meist auf dem Gerät anmelden, auf dem sie auch ihre E-Mails lesen. Beide schicken etwas ins Postfach, beide sind also genauso sicher wie das E-Mail-Konto. Der Unterschied liegt darin, was die Person als Nächstes tut.
Ein Magic Link ist ein Klick, wenn die E-Mail auf demselben Gerät aufgeht. In zwei häufigen Fällen scheitert er: Die Person startet am Laptop und öffnet die E-Mail auf dem Handy, oder ein Mail-Scanner der Firma öffnet den Link zuerst. Scalekit weist in seinem Vergleich von OTPs und Magic Links (opens in a new tab) darauf hin, dass Scanner einen einmaligen Link verbrauchen können, bevor die Person klickt. Ein defensives Muster ist ein Link, der eine Seite mit einem Button „Weiter“ öffnet, und erst der POST dieses Buttons verbraucht das Token.
Ein Code hält die Person in dem Tab, in dem sie angefangen hat, und funktioniert über Geräte hinweg. Der OTP Input im Block setzt autocomplete="one-time-code", so können iOS und Android den Code aus der Nachricht anbieten, und das Einfügen von „123 456“ füllt alle sechs Felder. Mit der sechsten Ziffer geht der Code automatisch raus.
Ist eine der beiden E-Mails verschickt, zeigt die Ansicht „Check your email“ die Adresse fett, einen Button „Open Gmail“ oder „Open Outlook“, wenn die Domain passt, einen Hinweis auf den Spam-Ordner und den Button zum erneuten Senden. Die Wartezeit kommt aus resendSeconds (standardmäßig 30) und liegt im Local Storage, ein Neuladen setzt sie also nicht zurück.
Wie baust du „Passwort vergessen“ und den Passwort-Reset?
Die Ansicht für ein vergessenes Passwort übernimmt die E-Mail, die im Anmeldeformular eingetippt wurde, so tippt niemand sie zweimal. onForgot verschickt den Link, und sobald das Promise erfüllt ist, wechselt der Block zur Ansicht „forgot-sent“.
Der Reset-Link öffnet eine eigene Route. Lies das Token auf dem Server und gib es an einen kleinen Client-Screen weiter, der mit der Ansicht reset startet:
resetPassword ist die letzte Server Action in der Datei mit den Actions oben. Die Seite prüft das Token, bevor sie irgendetwas rendert, weil der Block keine Ansicht für einen abgelaufenen Link hat. Läuft ein Token ab, während die Person tippt, scheitert das trotzdem sicher: Die Action gibt den Fehler zurück, und der Alert zeigt ihn. Die Reset-Ansicht zeigt eine Stärkeanzeige und eine Checkliste (Länge, Kleinbuchstabe, Großbuchstabe, Ziffer, Sonderzeichen), und onReset läuft erst, wenn das Passwort die Mindestlänge hat, mindestens die Stufe Fair erreicht und mit der Bestätigung übereinstimmt. passwordMinLength legt die Regel für die Länge fest (standardmäßig 8). OWASP stuft Passwörter unter 15 Zeichen ohne zweiten Faktor als schwach ein. Bietet deine App keine Zwei-Faktor-Bestätigung an, erhöh den Wert also.
In der letzten Ansicht steht „We signed you out on your other devices“. Sorg also auf dem Server dafür, dass das stimmt, und schick eine Bestätigung per E-Mail. Ihr Button „Sign in“ ruft onSuccess auf, und das führt zurück zur Anmelderoute, statt die Person automatisch anzumelden, so wie es die OWASP-Empfehlung zum Reset nahelegt.
$ npx wingo-ui@latest add login-pageWie passt du Layout und Texte der Login-Seite an?
Über Props: Für alles Visuelle gibt es eine, jeweils mit Standardwert.
layout:"split"(Formular neben einem Hero-Bereich ab 1024px),"centered"oder"card"(eine abgehobene Karte auf einem gepunkteten Hintergrund).heroImagelegt ein Foto hinter den Hero-Bereich,heroersetzt seinen Inhalt. Der Standard-Hero ist Beispieltext für ein erfundenes CRM, ersetz ihn also.logo,tone(standardmäßig neutral,"brand"für deine Akzentfarbe auf den primären Buttons),radius("sm","md","lg") undsize("lg", der Standard, bedeutet 48px hohe Felder;"md"bedeutet 40px, auf Touchscreens 44px).labelsnimmt jeden Text an und wird mitDEFAULT_LOGIN_PAGE_LABELSzusammengeführt. So übersetzt du die Seite.localesetzt nur das Attributlang, übersetzte Texte laufen also trotzdem überlabels.providerLayout="row"macht aus den gestapelten Buttons „Continue with Google“ Icon-Buttons nebeneinander.
Brauchst du nur ein Stück davon in einem eigenen Formular: Die Teile funktionieren auch einzeln. Ein Code-Schritt mit dem kostenlosen OTP Input:
Unser Guide zum React OTP Input geht genauer auf Einfügen, SMS-Autofill und Resend-Timer ein, und der Guide zu React-Formularkomponenten erklärt den Aufbau aus Feld, Label und Fehler, den der Block nutzt.
Was ist der nächste Schritt?
Installier die Blöcke und ersetz die simulierten Standard-Handler durch deine Server Actions:
Login Page, Register Page und Password Input gehören zu Wingo UI Pro. Das kostet Stand Oktober 2026 $8 im Monat, $80 im Jahr oder einmalig $150 (zu den Preisen); der OTP Input ist kostenlos. Für die Screens nach der Anmeldung baut das Tutorial zum Next.js Admin Dashboard die App-Shell, und die Kategorie „Praxis: Screens und Apps“ sammelt die anderen Tutorials für komplette Screens.
Häufige Fragen
Hat shadcn/ui eine Login-Seite?
Ja. Die Blocks-Seite von shadcn/ui führt Stand Oktober 2026 fünf Login-Blöcke (login-01 bis login-05) und fünf Signup-Blöcke. Das sind Layouts ohne Submit-Handler, ohne Fehlerzustand und ohne Ansichten für die Wiederherstellung, den Auth-Flow baust du also selbst.
Soll ein Login-Fehler verraten, ob die E-Mail existiert?
Nein. OWASP empfiehlt für jede fehlgeschlagene Anmeldung eine einzige allgemeine Meldung, etwa „Invalid user ID or password“, und im Formular für ein vergessenes Passwort dieselbe Antwort, egal ob es ein Konto gibt. Der Block Login Page hält sich standardmäßig an beide Regeln.
Magic Link oder Einmalcode: Was ist besser für passwortlosen Login?
Ein Code hält die Person in deinem Tab und übersteht Mail-Scanner in Firmen, die einen Link öffnen und ein einmaliges Token verbrauchen können, bevor die Person klickt. Ein Link ist ein Klick auf demselben Gerät. Die Login Page unterstützt beides über die Prop methods, du kannst also eins zuerst anbieten und das andere als Alternative.
Wie zeige ich Fehler aus Server Actions in einem Client-Login-Formular an?
Gib den Fehler aus der Server Action als Daten zurück, zum Beispiel { error: "E-Mail oder Passwort ist nicht korrekt" }, und wirf ihn im Client-Handler. Die Next.js-Doku rät, erwartbare Fehler als Rückgabewerte zu modellieren, und eine auf dem Server geworfene Meldung kommt in der Produktion nicht unverändert im Client an. Die Login Page zeigt die Meldung jeder Ablehnung in ihrem Alert.
Ist die Login-Seite von Wingo UI kostenlos?
Der OTP Input für die Codes ist kostenlos. Der Block Login Page, der Block Register Page und der Password Input gehören zu Wingo UI Pro, und alle lassen sich mit npx wingo-ui@latest add installieren.
- Authentifizierung
- Next.js
- shadcn/ui
- React
- Tailwind CSS


