Getting Started
Blocks
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
Implementation example:
import { OtpForm } from "@/components/better-auth-otp";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
export function LoginPage() {
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("Signed in successfully!");
window.location.href = "/dashboard";
}}
onError={(error) => toast.error(error)}
defaultEmail="user@example.com"
resendCooldown={60}
/>
);
}
"use client"
import { useState } from "react"
import { toast } from "sonner"
import { OtpForm } from "@/registry/nowts/blocks/better-auth-otp/components/better-auth-otp"
// Mock authClient for demo
const mockAuthClient = {
emailOtp: {
sendVerificationOtp: async ({ email }: { email: string; type: string }) => {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 1500))
// Simulate error for certain emails
if (email === "error@example.com") {
return { error: { message: "Failed to send OTP" } }
}
return { data: { success: true } }
},
},
signIn: {
emailOtp: async ({ email, otp }: { email: string; otp: string }) => {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 1000))
// Simulate success if OTP = "123456"
if (otp === "123456") {
return { data: { user: { email, id: "1" } } }
}
return { error: { message: "Invalid OTP code" } }
},
},
}
export function BetterAuthOtpDemo() {
const [isSuccess, setIsSuccess] = useState(false)
if (isSuccess) {
return (
<div className="w-[400px] rounded-lg border border-green-200 bg-green-50 p-6 dark:border-green-800 dark:bg-green-950/20">
<div className="space-y-3 text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
<svg
className="h-6 w-6 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 className="text-lg font-semibold text-green-800 dark:text-green-200">
Successfully signed in!
</h3>
<p className="text-sm text-green-600 dark:text-green-300">
You are now logged into your account.
</p>
<button
onClick={() => setIsSuccess(false)}
className="text-sm text-green-700 underline hover:no-underline dark:text-green-300"
>
Restart demo
</button>
</div>
</div>
)
}
return (
<div className="w-[400px] space-y-4">
<div className="space-y-2 text-center">
<h2 className="text-lg font-semibold">Better Auth OTP Demo</h2>
<p className="text-muted-foreground text-sm">
Enter your email to receive a verification code
</p>
</div>
<div className="bg-card rounded-lg border p-6">
<OtpForm
sendOtp={async (email) => {
const result = await mockAuthClient.emailOtp.sendVerificationOtp({
email,
type: "sign-in",
})
if (result.error) throw new Error(result.error.message)
}}
verifyOtp={async (email, otp) => {
const result = await mockAuthClient.signIn.emailOtp({ email, otp })
if (result.error) throw new Error(result.error.message)
}}
onSuccess={() => {
toast.success("Successfully signed in!")
setIsSuccess(true)
}}
onError={(error) => toast.error(error)}
defaultEmail="demo@example.com"
resendCooldown={10} // Reduced for demo
/>
</div>
<div className="text-muted-foreground bg-muted/30 space-y-1 rounded border p-3 text-xs">
<p>
<strong>💡 Demo tips:</strong>
</p>
<p>
• Use code <code className="bg-background rounded px-1">123456</code>{" "}
to simulate successful sign in
</p>
<p>
• Use{" "}
<code className="bg-background rounded px-1">error@example.com</code>{" "}
to test error handling
</p>
<p>• Cooldown is reduced to 10s for easier testing</p>
</div>
</div>
)
}
export function BetterAuthOtpCodeExample() {
return (
<div className="space-y-4">
<div className="text-foreground text-sm font-medium">
Implementation example:
</div>
<div className="relative">
<pre className="bg-muted/50 overflow-x-auto rounded-lg p-4 text-sm">
<code>{`import { OtpForm } from "@/components/better-auth-otp";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
export function LoginPage() {
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("Signed in successfully!");
window.location.href = "/dashboard";
}}
onError={(error) => toast.error(error)}
defaultEmail="user@example.com"
resendCooldown={60}
/>
);
}`}</code>
</pre>
</div>
</div>
)
}
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:
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
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
Prop | Type | Description |
---|---|---|
sendOtp | (email: string) => Promise<void> | Function to send the OTP |
verifyOtp | (email: string, otp: string) => Promise<void> | Function to verify the OTP |
onSuccess? | () => void | Callback called after successful verification |
onError? | (error: string) => void | Callback called on error |
defaultEmail? | string | Pre-filled email |
resendCooldown? | number | Cooldown 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 OTPemail-verification
: Email verificationforget-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));
}
Build Your SaaS in Days, Not Months
NOW.TS is the Next.js 15 boilerplate with everything you need to launch your SaaS—auth, payments, database, and AI-ready infrastructure.
Learn more about NOW.TS