A production-ready sign-in component with support for multiple authentication methods, built for Better Auth.
About
The Better Auth Sign In component provides a complete authentication experience with support for email/password, OTP (one-time password), and OAuth providers. It's built to work seamlessly with Better Auth but can be adapted to any authentication system.
Features
- ✅ Multiple auth methods: Email/password, OTP, and OAuth providers
- ✅ Smooth transitions: Animated method switching with Framer Motion
- ✅ Built-in OTP component: Automatically includes better-auth-otp
- ✅ Customizable providers: Easy OAuth configuration
- ✅ Responsive design: Mobile-first approach
- ✅ Complete TypeScript support
- ✅ Accessible: Built with shadcn/ui primitives
Preview
Acme Inc
"use client"
import { Github } from "lucide-react"
import { toast } from "sonner"
import { SignInPage } from "@/registry/nowts/blocks/better-auth-signin/sign-in-page"
export function BetterAuthSigninDemo() {
const handleSendOtp = async (email: string) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
toast.success(`OTP sent to ${email}`)
}
const handleVerifyOtp = async (email: string, otp: string) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
if (otp === "123456") {
toast.success("Successfully signed in!")
} else {
throw new Error("Invalid OTP code")
}
}
const handlePasswordSignIn = async (credentials: {
email: string
password: string
}) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
if (credentials.password === "password") {
toast.success("Successfully signed in!")
} else {
throw new Error("Invalid credentials")
}
}
const handleProviderSignIn = async (provider: string) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
toast.success(`Signing in with ${provider}`)
}
return (
<SignInPage
appName="Acme Inc"
description="Sign in to your account to continue"
onSendOtp={handleSendOtp}
onVerifyOtp={handleVerifyOtp}
onPasswordSignIn={handlePasswordSignIn}
onProviderSignIn={handleProviderSignIn}
providers={[
{
id: "github",
name: "GitHub",
icon: <Github className="size-4" />,
buttonClassName: "bg-black text-white hover:bg-gray-900",
},
]}
signUpUrl="#"
forgotPasswordUrl="#"
onError={(error) => toast.error(error)}
/>
)
}
Installation
1. Install the component
pnpm dlx shadcn@latest add https://ui.nowts.app/r/better-auth-signin.json
This will also install the better-auth-otp
component and all required dependencies.
2. Set up Better Auth
import { betterAuth } from "better-auth"
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
emailOtp: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
})
Usage
Basic usage with Better Auth
import { redirect } from "next/navigation"
import { Github } from "lucide-react"
import { auth } from "@/lib/auth"
import { SignInPage } from "@/components/sign-in-page"
export default function SignIn() {
async function handleSendOtp(email: string) {
"use server"
await auth.api.sendVerificationCode({
body: { email, type: "email-verification" },
})
}
async function handleVerifyOtp(email: string, otp: string) {
"use server"
const result = await auth.api.signInEmailOtp({
body: { email, otp },
})
if (result) {
redirect("/dashboard")
}
}
async function handlePasswordSignIn(credentials: {
email: string
password: string
}) {
"use server"
const result = await auth.api.signInEmail({
body: credentials,
})
if (result) {
redirect("/dashboard")
}
}
async function handleProviderSignIn(provider: string) {
"use server"
redirect(`/api/auth/social/${provider}`)
}
return (
<SignInPage
appName="Acme Inc"
appIcon="/logo.png"
onSendOtp={handleSendOtp}
onVerifyOtp={handleVerifyOtp}
onPasswordSignIn={handlePasswordSignIn}
onProviderSignIn={handleProviderSignIn}
providers={[
{
id: "github",
name: "GitHub",
icon: <Github className="size-4" />,
},
]}
signUpUrl="/signup"
forgotPasswordUrl="/forgot-password"
/>
)
}
OTP only
import { SignInPage } from "@/components/sign-in-page"
export default function SignIn() {
return (
<SignInPage
appName="Acme Inc"
defaultMethod="otp"
onSendOtp={async (email) => {
"use server"
// Send OTP logic
}}
onVerifyOtp={async (email, otp) => {
"use server"
// Verify OTP logic
}}
onPasswordSignIn={async () => {}} // Required but won't be shown
/>
)
}
With multiple OAuth providers
import { Github, Mail } from "lucide-react"
import { SignInPage } from "@/components/sign-in-page"
export default function SignIn() {
return (
<SignInPage
appName="Acme Inc"
onSendOtp={async (email) => {}}
onVerifyOtp={async (email, otp) => {}}
onPasswordSignIn={async ({ email, password }) => {}}
onProviderSignIn={async (provider) => {
"use server"
redirect(`/api/auth/${provider}`)
}}
providers={[
{
id: "github",
name: "GitHub",
icon: <Github className="size-4" />,
buttonClassName: "bg-black text-white hover:bg-gray-900",
},
{
id: "google",
name: "Google",
icon: <Mail className="size-4" />,
buttonClassName: "bg-white text-black border hover:bg-gray-50",
},
]}
/>
)
}
Custom styling
import { SignInPage } from "@/components/sign-in-page"
export default function SignIn() {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
<SignInPage
appName="Acme Inc"
description="Welcome back! Please sign in to continue."
onSendOtp={async (email) => {}}
onVerifyOtp={async (email, otp) => {}}
onPasswordSignIn={async ({ email, password }) => {}}
onProviderSignIn={async (provider) => {}}
/>
</div>
)
}
API
SignInPage
The main sign-in component.
type SignInPageProps = {
// App information
appName: string
appIcon?: string
description?: string
// Auth callbacks (all required)
onSendOtp: (email: string) => Promise<void>
onVerifyOtp: (email: string, otp: string) => Promise<void>
onPasswordSignIn: (credentials: {
email: string
password: string
}) => Promise<void>
onProviderSignIn: (providerId: string) => Promise<void>
// Optional configuration
defaultEmail?: string
defaultMethod?: "otp" | "password"
forgotPasswordUrl?: string
signUpUrl?: string
providers?: ProviderConfig[]
// Callbacks
onSuccess?: () => void
onError?: (error: string) => void
}
ProviderConfig
Configuration for OAuth providers.
type ProviderConfig = {
id: string
name: string
icon: React.ReactNode
buttonClassName?: string
}
Components
The sign-in block includes several sub-components:
SignInAuthMethods
Handles switching between OTP and password authentication.
SignInPasswordForm
Form for email/password sign in.
SignInProviderButton
Button for OAuth provider sign in.
Divider
Visual separator between auth methods.
Architecture
The Better Auth Sign In component is structured as:
- SignInPage - Main wrapper with app branding
- SignInAuthMethods - Auth method toggle (OTP ↔ Password)
- OtpForm - From better-auth-otp dependency
- SignInPasswordForm - Email/password form
- SignInProviderButton - OAuth providers
- Divider - Visual separation
Best practices
Error handling
import { toast } from "sonner"
import { SignInPage } from "@/components/sign-in-page"
export default function SignIn() {
return (
<SignInPage
appName="Acme Inc"
onError={(error) => {
toast.error(error)
}}
onSuccess={() => {
toast.success("Welcome back!")
}}
// ... other props
/>
)
}
Loading states
The component handles loading states internally for all auth methods.
Redirects
import { redirect } from "next/navigation"
import { SignInPage } from "@/components/sign-in-page"
export default function SignIn() {
async function handlePasswordSignIn(credentials) {
"use server"
const result = await auth.signIn(credentials)
if (result.success) {
// Get callback URL from searchParams if needed
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard"
redirect(callbackUrl)
}
}
return (
<SignInPage
onPasswordSignIn={handlePasswordSignIn}
// ... other props
/>
)
}
Troubleshooting
OTP not working
- Ensure
better-auth-otp
is properly installed - Check that email sending is configured in Better Auth
- Verify OTP expiration settings
Provider sign-in fails
- Check OAuth credentials in environment variables
- Verify redirect URLs in provider settings
- Ensure social providers are enabled in Better Auth
Forms don't submit
- All callback props (
onSendOtp
,onVerifyOtp
, etc.) are required - Check that callbacks are async functions
- Verify error handling in callbacks
On This Page
AboutFeaturesPreviewInstallation1. Install the component2. Set up Better AuthUsageBasic usage with Better AuthOTP onlyWith multiple OAuth providersCustom stylingAPISignInPageProviderConfigComponentsSignInAuthMethodsSignInPasswordFormSignInProviderButtonDividerArchitectureBest practicesError handlingLoading statesRedirectsTroubleshootingOTP not workingProvider sign-in failsForms don't submit