Sign in
Password or magic link, SSO and GitHub, inline validation and a check-your-email state.
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { ArrowLeft, Mail } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Field, Input, PasswordInput } from "@/components/ui/input";
type Mode = "password" | "magic" | "sent";
/** Sign in with password or a magic link, plus SSO and social providers. Validates inline. */
export function SignIn() {
const [mode, setMode] = React.useState<Mode>("password");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [errors, setErrors] = React.useState<{ email?: string; password?: string; form?: string }>({});
const [loading, setLoading] = React.useState(false);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
const next: typeof errors = {};
if (!/^\S+@\S+\.\S+$/.test(email)) next.email = "Enter a valid email address.";
if (mode === "password" && password.length < 8) next.password = "Passwords are at least 8 characters.";
setErrors(next);
if (Object.keys(next).length) return;
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
if (mode === "magic") setMode("sent");
else setErrors({ form: "That email and password don't match. Try again or use a magic link." });
};
return (
<div className="mx-auto w-full max-w-sm rounded-xl border border-border bg-background p-6 shadow-elev-2 sm:p-8">
<AnimatePresence mode="wait" initial={false}>
{mode === "sent" ? (
<motion.div key="sent" initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} className="text-center">
<span className="mx-auto grid size-12 place-items-center rounded-full bg-muted"><Mail className="size-5" /></span>
<h2 className="mt-4 text-xl font-semibold">Check your email</h2>
<p className="mt-2 text-sm text-muted-foreground">We sent a sign-in link to <span className="font-medium text-foreground">{email}</span>. It expires in 15 minutes.</p>
<Button variant="ghost" className="mt-6" onClick={() => setMode("magic")}><ArrowLeft /> Use a different email</Button>
</motion.div>
) : (
<motion.form key="form" onSubmit={submit} noValidate initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="space-y-5">
<div>
<h2 className="text-xl font-semibold">Sign in to Northwind</h2>
<p className="mt-1 text-sm text-muted-foreground">Welcome back.</p>
</div>
<div className="grid grid-cols-2 gap-2">
<Button type="button" variant="outline" className="h-10">
<svg viewBox="0 0 24 24" className="size-4" aria-hidden><path fill="currentColor" d="M12 .5a11.5 11.5 0 0 0-3.64 22.41c.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.52-1.33-1.28-1.69-1.28-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.76 2.69 1.25 3.35.96.1-.75.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.17 1.18a11 11 0 0 1 5.77 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.26 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 12 .5Z" /></svg>
GitHub
</Button>
<Button type="button" variant="outline" className="h-10">SSO</Button>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">or</div>
<Field label="Email" htmlFor="si-email" error={errors.email}>
<Input id="si-email" type="email" autoComplete="email" value={email} onValueChange={setEmail} invalid={!!errors.email} placeholder="you@company.com" />
</Field>
<AnimatePresence initial={false}>
{mode === "password" && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden">
<Field label="Password" htmlFor="si-pass" error={errors.password}>
<PasswordInput id="si-pass" value={password} onValueChange={setPassword} invalid={!!errors.password} />
</Field>
<button type="button" className="mt-2 text-xs text-muted-foreground underline-offset-4 hover:underline">Forgot password?</button>
</motion.div>
)}
</AnimatePresence>
{errors.form && <p role="alert" className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">{errors.form}</p>}
<Button type="submit" loading={loading} className="h-10 w-full">{mode === "password" ? "Sign in" : "Email me a link"}</Button>
<button type="button" onClick={() => { setMode(mode === "password" ? "magic" : "password"); setErrors({}); }} className="w-full text-center text-sm text-muted-foreground hover:text-foreground">
{mode === "password" ? "Sign in with a magic link instead" : "Use a password instead"}
</button>
</motion.form>
)}
</AnimatePresence>
</div>
);
}
export default SignIn;What's inside
- Inline validation
- Magic-link flow
- SSO and GitHub
- Error state
When to use
- Your app's sign-in page.
When not to use
- Sign-up flows that need more fields.
Installation
pnpm dlx shadcn@latest add https://lodeui.com/r/sign-in.jsonLands in components/blocks/sign-in.tsx with sample data you replace with your own.
The CLI also installs motion, lucide-react and every Lode component it's built from.
Usage
Import it from @/components/blocks/sign-in after installing. It ships with sample data, so it renders straight away; swap in your own data when you're ready.
import { SignIn } from "@/components/blocks/sign-in";
export default function Page() {
return (
<main className="grid min-h-dvh place-items-center p-6">
<SignIn />
</main>
);
}API reference
SignIn
Sign in with password or a magic link, plus SSO and GitHub, with inline validation and an error state. Also the default export. No props: sample data sits at the top of the file, ready to swap for your own.
No props.
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Move through fields and providers |
| Enter | Submit |
Accessibility
- Fields have labels and autocomplete; errors use role="alert".
Motion
- Switching to magic link crossfades the form; success slides in.
Responsive behavior
- Fits 320px; providers stack on phones.
Performance
- No dependencies beyond the button and input.
Notes for AI
- Replace the simulated submit with your auth client, e.g. signIn.email().
This page as Markdown, for assistants and coding agents:
/blocks/sign-in/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt