Better Auth OTP

PreviousNext

A clean, animated OTP authentication component for Better Auth's email OTP plugin.

About

The Better Auth OTP component is a modern and animated user interface for email OTP authentication, specially designed to work with Better Auth's Email OTP plugin.

Features

  • 2-step interface: Email input → OTP verification
  • Smooth animations with Framer Motion
  • Auto-validation when all 6 digits are entered
  • Cooldown system for code resending
  • Built-in error handling
  • Simple props API and flexible
  • Full TypeScript support

Preview

Installation

1. Install the component

pnpm dlx shadcn@latest add https://ui.nowts.app/r/better-auth-otp.json

2. Configure Better Auth

First, configure Better Auth with the Email OTP plugin:

auth.ts
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins"
 
export const auth = betterAuth({
  // ... other options
  plugins: [
    emailOTP({
      async sendVerificationOTP({ email, otp, type }) {
        // Implement email sending here
        // Example with Resend, Nodemailer, etc.
        await sendEmail({
          to: email,
          subject: "Your verification code",
          html: `Your OTP code is: <strong>${otp}</strong>`,
        })
      },
      otpLength: 6,
      expiresIn: 300, // 5 minutes
      resendCooldown: 60, // 1 minute
    }),
  ],
})

3. Configure the client

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { emailOTPClient } from "better-auth/client/plugins"
 
export const authClient = createAuthClient({
  baseURL: "http://localhost:3000",
  plugins: [emailOTPClient()],
})

Usage

Basic usage

import { toast } from "sonner"
 
import { authClient } from "@/lib/auth-client"
import { OtpForm } from "@/components/better-auth-otp"
 
export function SignInForm() {
  return (
    <OtpForm
      sendOtp={async (email) => {
        const result = await authClient.emailOtp.sendVerificationOtp({
          email,
          type: "sign-in",
        })
        if (result.error) throw new Error(result.error.message)
      }}
      verifyOtp={async (email, otp) => {
        const result = await authClient.signIn.emailOtp({ email, otp })
        if (result.error) throw new Error(result.error.message)
      }}
      onSuccess={() => {
        toast.success("Successfully signed in!")
        window.location.href = "/dashboard"
      }}
      onError={(error) => toast.error(error)}
    />
  )
}

Email verification

<OtpForm
  sendOtp={async (email) => {
    const result = await authClient.emailOtp.sendVerificationOtp({
      email,
      type: "email-verification",
    })
    if (result.error) throw new Error(result.error.message)
  }}
  verifyOtp={async (email, otp) => {
    const result = await authClient.emailOtp.verifyEmail({
      email,
      otp,
    })
    if (result.error) throw new Error(result.error.message)
  }}
  onSuccess={() => {
    toast.success("Email verified successfully!")
  }}
  onError={(error) => toast.error(error)}
/>

Password reset

<OtpForm
  sendOtp={async (email) => {
    const result = await authClient.emailOtp.sendVerificationOtp({
      email,
      type: "forget-password",
    })
    if (result.error) throw new Error(result.error.message)
  }}
  verifyOtp={async (email, otp) => {
    const result = await authClient.emailOtp.resetPassword({
      email,
      otp,
      password: newPassword, // Get the new password from a form
    })
    if (result.error) throw new Error(result.error.message)
  }}
  onSuccess={() => {
    toast.success("Password reset successfully!")
  }}
  onError={(error) => toast.error(error)}
/>

API

Props

PropTypeDescription
sendOtp(email: string) => Promise<void>Function to send the OTP
verifyOtp(email: string, otp: string) => Promise<void>Function to verify the OTP
onSuccess?() => voidCallback called after successful verification
onError?(error: string) => voidCallback called on error
defaultEmail?stringPre-filled email
resendCooldown?numberCooldown in seconds before being able to resend (default: 60)

Error handling

The component automatically handles errors and passes them via onError. Common errors include:

  • Invalid email
  • Incorrect OTP
  • Expired OTP
  • Maximum attempts exceeded
  • Network errors

Better Auth Integration

This component is specially designed for Better Auth's Email OTP plugin. It supports all OTP types:

  • sign-in: Sign in with OTP
  • email-verification: Email verification
  • forget-password: Password reset

Advanced Examples

With custom router

import { useRouter } from "next/navigation"
 
export function SignInForm() {
  const router = useRouter();
 
  return (
    <OtpForm
      sendOtp={...}
      verifyOtp={...}
      onSuccess={() => {
        router.push("/dashboard");
      }}
      onError={(error) => {
        // Log error or send to monitoring service
        console.error("OTP Error:", error);
        toast.error(error);
      }}
    />
  );
}

With state management

import { useAuthStore } from "@/store/auth"
 
export function SignInForm() {
  const { setUser } = useAuthStore();
 
  return (
    <OtpForm
      sendOtp={...}
      verifyOtp={async (email, otp) => {
        const result = await authClient.signIn.emailOtp({ email, otp });
        if (result.error) throw new Error(result.error.message);
 
        // Update store with user data
        setUser(result.data.user);
      }}
      onSuccess={() => {
        toast.success("Successfully signed in!");
      }}
    />
  );
}

Customization

The component uses shadcn/ui CSS classes and can be styled via CSS or Tailwind:

/* Customize animation */
.otp-form {
  --transition-duration: 0.3s;
}
 
/* Customize colors */
.otp-input {
  --border-color: hsl(var(--primary));
}