shadimage

Sign-in flow

Coordinates identity input, pending, error, and success while retaining entered context.

src/registry/components/sign-in-flow.tsx

"use client";

import { useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

export type SignInValues = { email: string; password: string };
export type SignInFlowProps = { title?: string; onSubmit?: (values: SignInValues) => void | Promise<void>; className?: string };

export function SignInFlow({ title = "Welcome back", onSubmit, className = "" }: SignInFlowProps) {
  const reduced = useReducedMotion() === true; const id = useId(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [status, setStatus] = useState<"idle" | "pending" | "error" | "success">("idle"); const [error, setError] = useState("");
  const submit = async () => { if (!email.includes("@") || password.length < 4) { setStatus("error"); setError("Enter a valid email and password."); return; } try { setStatus("pending"); setError(""); await onSubmit?.({ email, password }); setStatus("success"); } catch { setStatus("error"); setError("Sign in could not be completed. Try again."); } };
  return <form onSubmit={(event) => { event.preventDefault(); void submit(); }} className={`rounded-[18px] bg-[#1f2322] p-5 text-white shadow-[0_18px_46px_-28px_rgba(12,16,15,.82)] ${className}`}><span className="font-mono text-[10px] uppercase tracking-[.18em] text-[#a5c5a9]">Member access</span><h3 id={`${id}-title`} className="mt-4 text-3xl font-medium tracking-[-.05em]">{title}</h3><div className="mt-6 space-y-3"><label className="block text-[11px] text-white/65" htmlFor={`${id}-email`}>Email<input id={`${id}-email`} value={email} onChange={(event) => { setEmail(event.target.value); setStatus("idle"); }} type="email" autoComplete="email" className="mt-1.5 min-h-11 w-full rounded-lg border border-white/15 bg-white/[.06] px-3 text-[13px] text-white outline-none placeholder:text-white/32 focus-visible:ring-2 focus-visible:ring-[#9ed2a5]" placeholder="you@example.com" /></label><label className="block text-[11px] text-white/65" htmlFor={`${id}-password`}>Password<input id={`${id}-password`} value={password} onChange={(event) => { setPassword(event.target.value); setStatus("idle"); }} type="password" autoComplete="current-password" className="mt-1.5 min-h-11 w-full rounded-lg border border-white/15 bg-white/[.06] px-3 text-[13px] text-white outline-none focus-visible:ring-2 focus-visible:ring-[#9ed2a5]" /></label></div><button type="submit" disabled={status === "pending"} className="mt-5 min-h-11 w-full rounded-full bg-[#d9ebc8] px-4 text-[12px] font-medium text-[#243124] outline-none transition hover:bg-[#ecf6e3] disabled:opacity-60 focus-visible:ring-2 focus-visible:ring-white">{status === "pending" ? "Signing in…" : status === "success" ? "You’re in" : "Sign in"}</button><AnimatePresence>{status === "error" ? <motion.p role="alert" initial={reduced ? false : { opacity: 0, y: -5 }} animate={{ opacity: 1, y: 0 }} className="mt-3 text-[12px] text-[#ffb5a9]">{error}</motion.p> : null}</AnimatePresence></form>;
}
Where it goes
login
Use cases
saas-landing

DependenciesBundle weight · medium

animation
motion