All Taste components
Auth
Sign-in card
Sign in with Google or with an email and password, with a forgot-password link.
social button · divider · email + password · forgot link · sign-up footer
"use client";
import { useState } from "react";
import { Loader2 } from "lucide-react";
export function SignInCard({ product = "Saffron Studio", onSignIn, onGoogle }: {
product?: string;
onSignIn?: (v: { email: string; password: string; remember: boolean }) => Promise<void> | void;
onGoogle?: () => void;
}) {
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function submit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const f = new FormData(e.currentTarget);
setPending(true);
setError(null);
try {
await onSignIn?.({ email: String(f.get("email")), password: String(f.get("password")), remember: f.get("remember") === "on" });
} catch {
setError("That email and password don't match.");
} finally {
setPending(false);
}
}
const input = "h-9 w-full rounded border border-border-strong bg-bg px-3 text-sm outline-none placeholder:text-fg-subtle focus:border-primary focus:ring-[3px] focus:ring-primary/20";
return (
<div className="grid w-full max-w-[380px] gap-5 rounded-lg border border-border bg-bg p-7">
<div className="grid gap-3">
<span className="grid size-9 place-items-center rounded bg-primary text-[15px] font-semibold text-primary-fg" aria-hidden>{product[0]}</span>
<div>
<h2 className="text-xl font-medium tracking-tight">Sign in to {product}</h2>
<p className="mt-1 text-[13.5px] text-fg-muted">Welcome back. Pick up where you left off.</p>
</div>
</div>
<button type="button" onClick={onGoogle}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded border border-border-strong bg-bg text-sm font-medium hover:bg-surface">
<svg viewBox="0 0 24 24" className="size-4" fill="currentColor" aria-hidden>
<path d="M21.6 12.2c0-.7-.1-1.4-.2-2H12v3.8h5.4a4.6 4.6 0 0 1-2 3v2.5h3.2c1.9-1.7 3-4.3 3-7.3zM12 22c2.7 0 5-.9 6.6-2.4l-3.2-2.5c-.9.6-2 1-3.4 1-2.6 0-4.8-1.8-5.6-4.1H3.1v2.6A10 10 0 0 0 12 22zM6.4 14a6 6 0 0 1 0-3.9V7.4H3.1a10 10 0 0 0 0 9.2zM12 6c1.5 0 2.8.5 3.8 1.5l2.9-2.9A10 10 0 0 0 3.1 7.4l3.3 2.6C7.2 7.7 9.4 6 12 6z" />
</svg>
Continue with Google
</button>
<div className="flex items-center gap-3 text-xs text-fg-subtle before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">or</div>
<form onSubmit={submit} className="grid gap-3.5">
<div className="grid gap-1.5">
<label htmlFor="email" className="text-[13px] font-medium">Email</label>
<input id="email" name="email" type="email" required autoComplete="email" className={input} />
</div>
<div className="grid gap-1.5">
<div className="flex items-baseline justify-between">
<label htmlFor="password" className="text-[13px] font-medium">Password</label>
<a href="/forgot-password" className="text-[12.5px] text-primary hover:underline">Forgot password?</a>
</div>
<input id="password" name="password" type="password" required autoComplete="current-password"
aria-invalid={error ? true : undefined} aria-describedby={error ? "signin-error" : undefined} className={input} />
{error && <p id="signin-error" className="text-xs text-danger">{error}</p>}
</div>
<label className="inline-flex items-center gap-2 text-[13.5px]">
<input type="checkbox" name="remember" defaultChecked className="size-4 accent-primary" /> Keep me signed in
</label>
<button type="submit" disabled={pending}
className="inline-flex h-9 items-center justify-center gap-2 rounded bg-primary text-sm font-medium text-primary-fg hover:bg-primary-hover disabled:opacity-60">
{pending && <Loader2 className="size-4 animate-spin" aria-hidden />} Sign in
</button>
</form>
<p className="text-center text-[13px] text-fg-muted">
New here? <a href="/sign-up" className="text-primary hover:underline">Create an account</a>
</p>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.