Login Page 2

A modern, split-screen login page featuring social authentication, minimalist design, and a content-rich hero section with feature highlights.

Installation

$
npxshadcn@latest add @pagekit/login-page-2

Usage

Preview

A professional split-screen login page with Google and GitHub authentication, feature highlights, and responsive design.

Sign in to continue to your account

Or continue with email
Forgot password?

Don't have an account? Sign up for free

Features

  • ✅ Split-screen layout with login form and hero section
  • ✅ Social authentication (Google & GitHub)
  • ✅ Email and password login with icons
  • ✅ Full dark mode support
  • ✅ Password visibility toggle
  • ✅ Smooth Motion animations
  • ✅ Feature list with checkmarks
  • ✅ Testimonial section
  • ✅ Responsive design (mobile-first)
  • ✅ TypeScript support
  • ✅ Accessible with ARIA labels
  • ✅ Form validation ready

Component Structure

The login page is divided into two main sections:

Left Side - Login Form

Contains the authentication form with social login options and email/password inputs.

Right Side - Hero Section

Displays marketing content, feature highlights, and testimonials (visible on desktop only).

Background Pattern

Subtle dot pattern creates visual interest:

<div className="absolute inset-0 bg-gray-50 dark:bg-gray-950">
<div className="absolute inset-0 opacity-5 dark:opacity-10">
  <div
    className="absolute inset-0"
    style={{
      backgroundImage: 'radial-gradient(circle at 2px 2px, currentColor 1px, transparent 0)',
      backgroundSize: '40px 40px',
    }}
  />
</div>
</div>

Social Login Buttons

Pre-built Google and GitHub authentication buttons:

{/* Google Sign-in */}
<motion.button
type="button"
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white dark:bg-gray-900 border..."
whileTap={{ scale: 0.98 }}
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
  {/* Google SVG paths */}
</svg>
Continue with Google
</motion.button>

{/* GitHub Sign-in */}

<motion.button
type="button"
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-gray-900 dark:bg-white..."
whileTap={{ scale: 0.98 }}
>
<Github className="w-5 h-5" />
Continue with GitHub
</motion.button>

Email/Password Form

Form with icon-enhanced inputs:

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);

<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
  type="email"
  id="email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  className="w-full pl-10 pr-4 py-3 bg-white dark:bg-gray-900 border..."
  placeholder="Enter your email"
  required
/>
</div>

<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
  type={showPassword ? "text" : "password"}
  id="password"
  value={password}
  onChange={(e) => setPassword(e.target.value)}
  className="w-full pl-10 pr-12 py-3..."
  placeholder="Enter your password"
  required
/>
<button
  type="button"
  onClick={() => setShowPassword(!showPassword)}
  className="absolute right-3 top-1/2 -translate-y-1/2..."
>
  {showPassword ? (
    <EyeOff className="w-5 h-5" />
  ) : (
    <Eye className="w-5 h-5" />
  )}
</button>
</div>

Feature List

Animated feature highlights in the hero section:

<motion.div
className="flex items-start gap-3"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<div className="w-8 h-8 rounded-lg bg-gray-300 dark:bg-gray-700 flex items-center justify-center">
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
  </svg>
</div>
<div>
  <h3 className="font-semibold text-gray-900 dark:text-white mb-1">
    Lightning Fast Performance
  </h3>
  <p className="text-sm text-gray-600 dark:text-gray-400">
    Optimized for speed and efficiency
  </p>
</div>
</motion.div>

Customization

Color Theme

The design uses a neutral grayscale palette that adapts to any brand:

// Background
className="bg-gray-50 dark:bg-gray-950"

// Buttons (primary)
className="bg-gray-900 dark:bg-white text-white dark:text-gray-900"

// Inputs
className="bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700"

// Text colors
className="text-gray-900 dark:text-white" // Primary text
className="text-gray-600 dark:text-gray-400" // Secondary text

Brand Customization

Replace "PageKit" with your brand name:

<h2 className="text-5xl font-bold text-gray-900 dark:text-white mb-6">
Welcome to YourBrand
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
Your custom tagline here
</p>

Feature Items

Customize the feature list to match your product:

const features = [
{
  title: "Your Feature 1",
  description: "Feature description here",
  delay: 0.4
},
{
  title: "Your Feature 2",
  description: "Feature description here",
  delay: 0.5
},
{
  title: "Your Feature 3",
  description: "Feature description here",
  delay: 0.6
}
];

Layout Adjustments

Change the split-screen ratio:

{/* 50/50 split (default) */}
<div className="w-full lg:w-1/2">...</div>
<div className="hidden lg:flex lg:w-1/2">...</div>

{/* 40/60 split */}

<div className="w-full lg:w-2/5">...</div>
<div className="hidden lg:flex lg:w-3/5">...</div>

{/* Full width on mobile, fixed width on desktop */}

<div className="w-full lg:max-w-md">...</div>

Social Authentication

Add more social providers:

{/* Apple Sign-in */}
<motion.button
type="button"
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-black text-white border border-black rounded-lg"
whileTap={{ scale: 0.98 }}
onClick={handleAppleLogin}
>
<Apple className="w-5 h-5" />
Continue with Apple
</motion.button>

{/* Microsoft Sign-in */}

<motion.button
type="button"
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white border border-gray-300 rounded-lg"
whileTap={{ scale: 0.98 }}
onClick={handleMicrosoftLogin}
>
<Microsoft className="w-5 h-5" />
Continue with Microsoft
</motion.button>

Form Handling

Implement authentication logic:

const LoginPage = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");

const handleEmailLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError("");

  try {
    await signIn("credentials", {
      email,
      password,
      redirect: false,
    });
    // Handle success
  } catch (err) {
    setError("Invalid credentials. Please try again.");
  } finally {
    setIsLoading(false);
  }

};

const handleGoogleLogin = async () => {
await signIn("google", { callbackUrl: "/dashboard" });
};

const handleGithubLogin = async () => {
await signIn("github", { callbackUrl: "/dashboard" });
};

return (
// ... component JSX
);
};

Add Error Handling

Display validation and authentication errors:

{error && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
  <p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}

{/* Email validation */}

<input
type="email"
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
required
onInvalid={(e) => {
  e.currentTarget.setCustomValidity("Please enter a valid email address");
}}
onInput={(e) => {
  e.currentTarget.setCustomValidity("");
}}
/>

Loading States

Add loading indicators during authentication:

<motion.button
type="submit"
disabled={isLoading}
className="w-full bg-gray-900 dark:bg-white text-white dark:text-gray-900 py-3 px-4 rounded-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
whileTap={{ scale: isLoading ? 1 : 0.98 }}
>
{isLoading ? (
  <span className="flex items-center justify-center gap-2">
    <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
      <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
      <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
    </svg>
    Signing in...
  </span>
) : (
  "Sign in"
)}
</motion.button>

Responsive Behavior

The component is fully responsive with a mobile-first approach:

  • Mobile (< 1024px): Single column layout with login form only
  • Desktop (≥ 1024px): Split-screen layout with hero section visible
  • Max-width container: Content limited to 1600px for ultra-wide screens

Mobile-Only Layout

To show a simplified version on mobile:

{/* Hide social buttons on mobile */}
<div className="hidden sm:block space-y-3 mb-6">
{/* Social login buttons */}
</div>

{/* Hide divider on mobile */}

<div className="hidden sm:block relative mb-6">
{/* Or continue with email divider */}
</div>

Dark Mode

The component uses Tailwind's dark mode classes and automatically adapts to the user's system preferences.

Dark Mode Colors

// Background
dark:bg-gray-950

// Card/Input backgrounds
dark:bg-gray-900

// Borders
dark:border-gray-700

// Text
dark:text-white // Primary
dark:text-gray-300 // Secondary
dark:text-gray-400 // Tertiary

// Buttons
dark:bg-white // Primary button
dark:text-gray-900 // Primary button text

Dependencies

  • motion - Animation library for smooth transitions
  • lucide-react - Icon library (Eye, EyeOff, Mail, Lock, Github)
  • clsx - Utility for conditional classNames
  • tailwind-merge - Merge Tailwind classes without conflicts

Integration Examples

Next.js with NextAuth

import { signIn } from "next-auth/react"

const handleGoogleLogin = async () => {
await signIn("google", { callbackUrl: "/dashboard" })
}

const handleEmailLogin = async (e: React.FormEvent) => {
e.preventDefault()
const result = await signIn("credentials", {
email,
password,
redirect: false,
})

if (result?.error) {
setError(result.error)
} else {
router.push("/dashboard")
}
}

With Form Validation (React Hook Form)

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"

const loginSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
})

type LoginFormData = z.infer<typeof loginSchema>

const LoginPage = () => {
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
})

const onSubmit = async (data: LoginFormData) => {
// Handle login
}

return (

<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}

    <input {...register("password")} type="password" />
    {errors.password && <p>{errors.password.message}</p>}
  </form>

)
}

Accessibility

The component includes several accessibility features:

  • ARIA labels on password toggle button
  • Semantic HTML with proper form elements
  • Focus states on all interactive elements
  • Keyboard navigation support
  • Screen reader friendly labels and hints

Enhanced Accessibility

{/* Add form labels for screen readers */}
<label htmlFor="email" className="sr-only">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid={!!errors.email}
/>
{errors.email && (
<p id="email-error" role="alert" className="text-red-600 text-sm mt-1">
  {errors.email.message}
</p>
)}

{/* Announce loading state */}

<button type="submit" aria-busy={isLoading} aria-live="polite">
{isLoading ? "Signing in..." : "Sign in"}
</button>