Skip to content
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

Sign in to Saffron Studio

Welcome back. Pick up where you left off.

or

New here? Create an account

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