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

> So baust du eine shadcn login page in Next.js: Passwort, Magic Link, E-Mail-Codes, Social Login, 2FA und Passwort-Reset, inklusive aller Fehlerzustände.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build)
- Lesezeit: 12 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/shadcn-login-page
- Englische Fassung: https://wingo-ui.com/blog/shadcn-login-page

## Kurz gesagt

Eine produktionsreife shadcn login page braucht acht Ansichten (Anmelden, Link gesendet, E-Mail-Code, Zwei-Faktor-Code, Passwort vergessen, Reset-Link gesendet, neues Passwort, fertig) und für jede einen Fehlerzustand. Die fünf offiziellen Login-Blöcke von shadcn/ui sind Stand Oktober 2026 statische Formulare ohne Submit-Handler, diese Logik schreibst du also selbst. Dieses Tutorial verbindet den Block Login Page von Wingo UI, der alle acht Ansichten samt Fehlern mitbringt, mit Next.js Server Actions, die Fehler als Daten zurückgeben.

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](https://ui.shadcn.com/blocks/login)). Galerien wie [shadcnblocks](https://www.shadcnblocks.com/blocks/login) und [Tailark](https://tailark.com/blog/login-blocks) 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](https://wingo-ui.com/components/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:

1. **Anmelden.** E-Mail, Passwort, „Angemeldet bleiben“, ein Link für ein vergessenes Passwort, Social Buttons und ein Wechsel zu einer passwortlosen Methode.
2. **Schau in dein Postfach.** Nach einem Magic Link: die Adresse, ein Button zum erneuten Senden, der wartet, und ein Weg zu einer anderen Adresse.
3. **Code eingeben.** Ein 6-stelliger Code per E-Mail für die passwortlose Anmeldung.
4. **Zwei-Faktor-Bestätigung.** Der Code aus der Authenticator-App nach einem richtigen Passwort, mit Backup-Code als Ausweg.
5. **Passwort vergessen.** Ein E-Mail-Feld.
6. **Reset-Link gesendet.** Eine Meldung, die nie bestätigt, ob das Konto existiert.
7. **Neues Passwort festlegen.** Regeln für die Passwortstärke und ein Feld zur Bestätigung.
8. **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.

> Live-Demo (Login Page): Melde dich mit einer beliebigen E-Mail und einem Passwort unter 8 Zeichen an, dann siehst du den Fehler-Alert. Drück danach auf Forgot password: Die eingegebene E-Mail wird mitgenommen. Probier sie unter [Login Page](https://wingo-ui.com/components/login-page) aus und installier sie mit `npx wingo-ui@latest add login-page`.

## Wie installierst du den Login-Page-Block?

Ein CLI-Befehl kopiert den Block und alles, was er importiert, in dein Projekt:

```bash
npx wingo-ui@latest add login-page
```

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](https://wingo-ui.com/components/password-input) für den Umschalter zum Einblenden des Passworts und die Warnung bei aktiver Feststelltaste, [OTP Input](https://wingo-ui.com/components/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](https://nextjs.org/docs/app/guides/authentication) nutzt Server Actions für die Anmeldelogik, weil sie immer auf dem Server laufen. Der [Guide zur Fehlerbehandlung](https://nextjs.org/docs/app/getting-started/error-handling) 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](https://nextjs.org/docs/app/api-reference/file-conventions/error)). 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

```ts
// app/(auth)/actions.ts
"use server";

import type { SignInValues, VerifyCodeContext } from "@/components/blocks/login-page";
// ein dünner Wrapper, den du um Better Auth, Auth.js oder deinen eigenen Session-Code schreibst
import { auth } from "@/lib/auth";

export type ActionResult = { error?: string; twoFactor?: boolean };

export async function signIn(values: SignInValues): Promise<ActionResult> {
  const result = await auth.signInWithPassword(values);
  if (!result.ok) return { error: "E-Mail oder Passwort ist nicht korrekt" };
  return { twoFactor: result.needsSecondFactor };
}

export async function sendMagicLink(email: string): Promise<ActionResult> {
  await auth.sendMagicLink(email);
  return {};
}

export async function sendCode(email: string): Promise<ActionResult> {
  await auth.sendEmailCode(email);
  return {};
}

export async function verifyCode(code: string, context: VerifyCodeContext): Promise<ActionResult> {
  // two-factor prüft die offene Anmeldung, die signIn gestartet hat; otp prüft den per E-Mail geschickten Code
  const result = await auth.verifyCode({ code, kind: context.view, backup: context.backup });
  if (!result.ok) return { error: `Falscher Code. Noch ${result.attemptsLeft} Versuche.` };
  return {};
}

export async function requestReset(email: string): Promise<ActionResult> {
  // dieselbe Antwort, egal ob das Konto existiert
  await auth.requestPasswordReset(email);
  return {};
}

export async function resetPassword(token: string, password: string): Promise<ActionResult> {
  const result = await auth.resetPassword({ token, password });
  if (!result.ok) return { error: "Dieser Reset-Link ist abgelaufen. Fordere einen neuen an." };
  return {};
}
```

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

```tsx
// app/(auth)/login/login-screen.tsx
"use client";

import { useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { AUTH_PROVIDERS, LoginPage, type LoginView } from "@/components/blocks/login-page";
import { requestReset, sendCode, sendMagicLink, signIn, verifyCode, type ActionResult } from "../actions";

// ein zurückgegebener Fehler wird zur Ablehnung, also zeigt der Block ihn in seinem Alert
function check(result: ActionResult) {
  if (result.error) throw new Error(result.error);
  return result;
}

export function LoginScreen() {
  const router = useRouter();
  const [view, setView] = useState<LoginView>("sign-in");
  // von signIn gesetzt, wenn dieses Konto die Zwei-Faktor-Bestätigung aktiviert hat
  const needsCode = useRef(false);

  return (
    <LoginPage
      view={view}
      onViewChange={setView}
      methods={["password", "magic-link", "otp"]}
      providers={AUTH_PROVIDERS.filter((provider) => provider.id !== "microsoft")}
      linkComponent={Link}
      registerHref="/signup"
      onSignIn={async (values) => {
        needsCode.current = check(await signIn(values)).twoFactor === true;
      }}
      onMagicLink={async (email) => check(await sendMagicLink(email))}
      onSendCode={async (email) => check(await sendCode(email))}
      onVerifyCode={async (code, context) => check(await verifyCode(code, context))}
      onForgot={async (email) => check(await requestReset(email))}
      onProvider={(id) => {
        window.location.assign(`/api/auth/${id}`);
        // wird nie abgeschlossen: Der Spinner dreht sich weiter, während der Browser die Seite verlässt
        return new Promise(() => {});
      }}
      onSuccess={() => {
        if (needsCode.current) {
          needsCode.current = false;
          setView("two-factor");
          return;
        }
        router.push("/dashboard");
      }}
    />
  );
}
```

```tsx
// app/(auth)/login/page.tsx
import { LoginScreen } from "./login-screen";

export default function Page() {
  return <LoginScreen />;
}
```

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](https://wingo-ui.com/components/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.

| Situation | Was die Person sieht |
|---|---|
| Falsche E-Mail oder falsches Passwort | Roter Alert über dem Formular, die E-Mail bleibt, der Fokus springt zurück aufs Passwort; beim zweiten Fehlschlag wird auch das Passwort geleert |
| Feststelltaste beim Tippen des Passworts aktiv | Eine bernsteinfarbene Warnung „Caps Lock is on“ unter dem Feld |
| Netzwerk- oder unbekannter Fehler | „Something went wrong. Try again.“, wenn der Fehler keine Meldung hat |
| Anmeldung beim Provider scheitert | Der Spinner auf dem gedrückten Button stoppt, und der Alert zeigt die Meldung |
| Falscher Code | Die Felder schütteln sich, und der Alert zeigt deine Meldung, etwa „Falscher Code. Noch 2 Versuche.“ |
| Drei falsche Codes | Das Feld sperrt sich für 60 Sekunden und zählt live herunter: „Too many attempts. Try again in 0:59.“ |
| „Erneut senden“ zu früh gedrückt | Der Button wartet mit „Resend in 0:24“, und die Wartezeit übersteht ein Neuladen |
| Passwort vergessen für eine unbekannte E-Mail | Dieselbe Meldung „If there is an account for...“ wie für ein echtes Konto |
| Schwaches oder abweichendes neues Passwort | Stärkeanzeige, Checkliste der Regeln und eine Bestätigung, die übereinstimmen muss |

Zwei davon folgen direkt dem [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html): Fehlgeschlagene Anmeldungen bekommen eine einzige allgemeine Meldung, die nie verrät, welcher Teil falsch war, und das [Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html) 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.

> Live-Demo (Login Page): Tipp 123456 ein, um durchzukommen, oder dreimal einen falschen Code für das Schütteln, die verbleibenden Versuche und die Sperre für eine Minute. Wechsle danach zu einem Backup-Code. Probier sie unter [Login Page](https://wingo-ui.com/components/login-page) aus und installier sie mit `npx wingo-ui@latest add login-page`.

## Magic 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](https://www.scalekit.com/blog/otp-vs-magic-links-passwordless-authentication) 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:

```tsx
// app/(auth)/reset-password/page.tsx
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { ResetScreen } from "./reset-screen";

export default async function Page({ searchParams }: { searchParams: Promise<{ token?: string | string[] }> }) {
  const { token } = await searchParams;
  // der Block hat keine Ansicht „Link abgelaufen“, also geht ein ungültiges Token zurück zur Anmeldung
  if (typeof token !== "string" || !(await auth.isResetTokenValid(token))) redirect("/login");
  return <ResetScreen token={token} />;
}
```

```tsx
// app/(auth)/reset-password/reset-screen.tsx
"use client";

import { useRouter } from "next/navigation";
import { LoginPage } from "@/components/blocks/login-page";
import { resetPassword } from "../actions";

export function ResetScreen({ token }: { token: string }) {
  const router = useRouter();
  return (
    <LoginPage
      defaultView="reset"
      passwordMinLength={12}
      onReset={async (password) => {
        const result = await resetPassword(token, password);
        if (result.error) throw new Error(result.error);
      }}
      onSuccess={() => router.push("/login")}
    />
  );
}
```

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

> Live-Demo (Login Page): Drück Change password mit leeren Feldern, um jeden Fehler zu sehen, und tipp dann ein neues Passwort ein: Stärkeanzeige und Checkliste reagieren sofort. Probier sie unter [Login Page](https://wingo-ui.com/components/login-page) aus und installier sie mit `npx wingo-ui@latest add login-page`.

## Wie 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).
- `heroImage` legt ein Foto hinter den Hero-Bereich, `hero` ersetzt 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"`) und `size` (`"lg"`, der Standard, bedeutet 48px hohe Felder; `"md"` bedeutet 40px, auf Touchscreens 44px).
- `labels` nimmt jeden Text an und wird mit `DEFAULT_LOGIN_PAGE_LABELS` zusammengeführt. So übersetzt du die Seite. `locale` setzt nur das Attribut `lang`, übersetzte Texte laufen also trotzdem über `labels`.
- `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:

```tsx
"use client";

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

export function CodeStep({ verify }: { verify: (code: string) => Promise<boolean> }) {
  const [status, setStatus] = useState<OtpStatus>("idle");
  return (
    <OtpInput
      length={6}
      autoFocus
      status={status}
      onChange={() => setStatus("idle")}
      onComplete={async (code) => setStatus((await verify(code)) ? "success" : "error")}
    />
  );
}
```

Unser [Guide zum React OTP Input](https://wingo-ui.com/de/blog/react-otp-input) geht genauer auf Einfügen, SMS-Autofill und Resend-Timer ein, und der [Guide zu React-Formularkomponenten](https://wingo-ui.com/de/blog/react-form-components-guide) 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:

```bash
npx wingo-ui@latest add login-page register-page
```

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](https://wingo-ui.com/de/pricing)); der OTP Input ist kostenlos. Für die Screens nach der Anmeldung baut das [Tutorial zum Next.js Admin Dashboard](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial) die App-Shell, und die [Kategorie „Praxis: Screens und Apps“](https://wingo-ui.com/de/blog/category/build) sammelt die anderen Tutorials für komplette Screens.

## Komponenten in diesem Artikel

- [Login Page](https://wingo-ui.com/components/login-page) (Pro): A React login page with password, magic link or code sign-in, Google, Apple and Microsoft buttons, two-step codes and password recovery. Installation: `npx wingo-ui@latest add login-page`
- [Register Page](https://wingo-ui.com/components/register-page) (Pro): A React register page for a CRM sign-up: an account with password rules, company details from the CUI, an email code and a welcome step. Installation: `npx wingo-ui@latest add register-page`
- [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`
- [Password Input](https://wingo-ui.com/components/password-input) (Pro): A password field with a caret-safe reveal toggle, a Caps Lock warning and, for sign-up, a strength meter, rules checklist and generator. Installation: `npx wingo-ui@latest add password-input`

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

---

Quelle: https://wingo-ui.com/de/blog/shadcn-login-page
