BTTR Blocks LogoBTTR Blocks
Authentication

Login Page

The <LoginForm /> offers a simple and intuitive interface for users to securely log in to their accounts.

Installation

Copy and paste the following code into your project.

login-form.tsx
"use client";

import { cn } from "@/lib/utils"

import * as React from "react"
import { useState } from "react";
import * as z from "zod"
import { FaGoogle, FaGithub } from "react-icons/fa";
import { useRouter } from "next/navigation";
import { toast } from "sonner"

import { useForm } from "@tanstack/react-form"

import { authClient } from "@/lib/auth/client";


import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  email: z
    .email()
    .min(1, "Email cannot be empty."),
  password: z
    .string()
    .min(1, "Password cannot be empty.")
})

export function LoginForm({
  className,
  ...props
}: React.ComponentProps<"div">) {
  const callbackURL = "/success";
  const router = useRouter();

  const [isGoogleLoading, setIsGoogleLoading] = useState(false);
  const [isGithubLoading, setIsGithubLoading] = useState(false);
  const [isEmailLoading, setIsEmailLoading] = useState(false);
  
  const form = useForm({
    defaultValues: {
      email: "",
      password: "",
    },
    validators: {
      onSubmit: formSchema,
    },
    onSubmit: async ({ value }) => {
      setIsEmailLoading(true);
  
      try {
        await authClient.signIn.email(
          {
            email: value.email.toLowerCase(),
            password: value.password,
          },
          {
            onSuccess: (_ctx) => {
              toast.success("Welcome!");
              router.push(callbackURL);
            },
            onError: (ctx) => {
              if (ctx.error.status === 403) {
                toast.error("Please verify your email address");
              }
            },
          }
        );
      } catch (_error) {
        return toast("Login failed. Please try again.");
      } finally {
        setIsEmailLoading(false);
      }
    },
  })
  
  const handleSocialSignIn = async (provider: "google" | "github") => {
    if (provider === "google") {
      setIsGoogleLoading(true);
    } else {
      setIsGithubLoading(true);
    }

    try {
      await authClient.signIn.social({
        provider,
        callbackURL
      });
    } catch (_error) {
      return toast("Sign in failed. Please try again.");
    } finally {
      if (provider === "google") {
        setIsGoogleLoading(false);
      } else {
        setIsGithubLoading(false);
      }
    }
  };
    
  return (
    <div className={cn("flex flex-col gap-6", className)} {...props}>
      <Card>
        <CardHeader>
          <CardTitle>Login to your account</CardTitle>
          <CardDescription>
            Enter your email below to login to your account
          </CardDescription>
        </CardHeader>
        <CardContent>
          <form 
            id="login-form" 
            onSubmit={(e) => { 
              e.preventDefault() 
              form.handleSubmit() 
            }}
          >
            <FieldGroup>
              <form.Field
                name="email"
                children={(field) => {
                  const isInvalid =
                    field.state.meta.isTouched && !field.state.meta.isValid
                  return (
                    <Field data-invalid={isInvalid}>
                      <FieldLabel htmlFor="email">Email</FieldLabel>
                      <Input
                        id={field.name}
                        name={field.name}
                        value={field.state.value}
                        onBlur={field.handleBlur}
                        onChange={(e) => field.handleChange(e.target.value)}
                        aria-invalid={isInvalid}
                        type="email"
                        placeholder="Enter your email..."
                        required
                      />
                      {isInvalid && (
                        <FieldError errors={field.state.meta.errors} />
                      )}
                    </Field>
                  )
                }}
              />
              <form.Field
                name="password"
                children={(field) => {
                  const isInvalid =
                    field.state.meta.isTouched && !field.state.meta.isValid
                  return (
                    <Field data-invalid={isInvalid}>
                      <div className="flex items-center">
                        <FieldLabel htmlFor="password">Password</FieldLabel>
                        <a
                          href="/forgot-password"
                          className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
                        >
                          Forgot your password?
                        </a>
                      </div>
                      <Input 
                        id={field.name}
                        name={field.name}
                        value={field.state.value}
                        onBlur={field.handleBlur}
                        onChange={(e) => field.handleChange(e.target.value)}
                        aria-invalid={isInvalid}
                        type="password" 
                        placeholder="Enter your password..."
                        required 
                      />
                      {isInvalid && (
                        <FieldError errors={field.state.meta.errors} />
                      )}
                    </Field>
                  )
                }}
              />
              <Field>
                <Button 
                  type="submit"
                  disabled={isEmailLoading || isGoogleLoading || isGithubLoading} 
                >
                  Login
                  {isEmailLoading ? "Loading..." : "Login"}
                </Button>
                <Button 
                  variant="outline" 
                  type="button"
                  disabled={isEmailLoading || isGoogleLoading || isGithubLoading} 
                  onClick={async () => handleSocialSignIn("google")}
                >
                  {isGoogleLoading ? (
                    "Loading..."
                  ) : (
                    <>
                      <FaGoogle />
                      <span className="sr-only">Login with Google</span>
                    </>
                  )}
                </Button>
                <Button 
                  variant="outline" 
                  type="button"
                  disabled={isEmailLoading || isGoogleLoading || isGithubLoading} 
                  onClick={async () => handleSocialSignIn("github")}
                >
                  {isGithubLoading ? (
                    "Loading..."
                  ) : (
                    <>
                      <FaGithub />
                      <span className="sr-only">Login with Github</span>
                    </>
                  )}
                </Button>
                <FieldDescription className="text-center">
                  Don&apos;t have an account? <a href="/sign-up">Sign up</a>
                </FieldDescription>
              </Field>
            </FieldGroup>
          </form>
        </CardContent>
      </Card>
    </div>
  )
}

Usage

Here's how you can include the <LoginForm /> in your project

page.tsx
import { Suspense } from "react"
import { LoginForm } from "@/components/login-form"
import { Skeleton } from "@/components/ui/skeleton"

export const metadata: Metadata = {
  title: "Log In",
};

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <Suspense>
          <LoginForm />
        </Suspense>
      </div>
    </div>
  );
}

Inspired by the shadcn login block!

On this page