Tailwind css & shadcn step form based register page
import { useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Link, useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { format } from "date-fns"
import {
CalendarIcon,
Loader2,
Eye,
EyeOff,
ShieldCheck,
HeartPulse,
ArrowLeft,
ArrowRight,
} from "lucide-react"
import { cn, NativeSelect } from "@hms/shared"
import { Button } from "@hms/shared/components/ui/button"
import { Calendar } from "@hms/shared/components/ui/calendar"
import { Input } from "@hms/shared/components/ui/input"
import { Label } from "@hms/shared/components/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@hms/shared/components/ui/popover"
const STEPS = [
{ title: "Personal Info", description: "Tell us about yourself" },
{ title: "Contact Details", description: "How can we reach you?" },
{ title: "Security", description: "Set up your password and preferences" },
] as const
const stepOneSchema = z.object({
fullName: z.string().min(2, "Full name is required"),
gender: z.string().min(1, "Please select a gender"),
dateOfBirth: z.string().min(1, "Date of birth is required"),
})
const stepTwoSchema = z.object({
phone: z.string().min(10, "Enter a valid phone number"),
email: z.string().email("Enter a valid email").optional().or(z.literal("")),
})
const registerSchema = z
.object({
fullName: z.string().min(2, "Full name is required"),
gender: z.string().min(1, "Please select a gender"),
dateOfBirth: z.string().min(1, "Date of birth is required"),
phone: z.string().min(10, "Enter a valid phone number"),
email: z.string().email("Enter a valid email").optional().or(z.literal("")),
password: z.string().min(6, "Password must be at least 6 characters"),
confirmPassword: z.string(),
bloodGroup: z.string().optional(),
city: z.string().optional(),
})
.refine((d) => d.password === d.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
})
type RegisterFormValues = z.infer<typeof registerSchema>
export default function SplitRegister() {
const navigate = useNavigate()
const [step, setStep] = useState(0)
const [dobOpen, setDobOpen] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const {
register,
handleSubmit,
setValue,
watch,
trigger,
formState: { errors, isSubmitting },
} = useForm<RegisterFormValues>({
resolver: zodResolver(registerSchema),
mode: "onTouched",
})
const dobValue = watch("dateOfBirth")
async function onSubmit(data: RegisterFormValues) {
await new Promise((r) => setTimeout(r, 1000))
localStorage.setItem("access_token", "mock-token")
localStorage.setItem(
"user",
JSON.stringify({
name: data.fullName,
email: data.email || data.phone,
role: "PATIENT",
})
)
toast.success("Account created successfully!")
navigate("/dashboard")
}
async function handleNext() {
if (step === 0) {
const fields = Object.keys(stepOneSchema.shape) as (keyof z.infer<typeof stepOneSchema>)[]
const valid = await trigger(fields)
if (!valid) return
} else if (step === 1) {
const fields = Object.keys(stepTwoSchema.shape) as (keyof z.infer<typeof stepTwoSchema>)[]
const valid = await trigger(fields)
if (!valid) return
}
setStep((s) => Math.min(s + 1, STEPS.length - 1))
}
function handleBack() {
setStep((s) => Math.max(s - 1, 0))
}
const inputClass =
"h-11 rounded-lg border-zinc-300 bg-white text-zinc-900 placeholder:text-zinc-400 focus-visible:border-zinc-900 focus-visible:ring-zinc-900/20"
const selectClass =
"h-11 rounded-lg border-zinc-300 bg-white text-zinc-900 focus-visible:border-zinc-900 focus-visible:ring-zinc-900/20"
return (
<div className="flex min-h-svh">
{/* Left Panel - Dark Branding */}
<div className="hidden lg:flex lg:w-1/2 bg-zinc-950 text-white flex-col justify-between p-10">
<div>
<h1 className="text-3xl font-bold tracking-tight">Get Started</h1>
<p className="mt-2 text-zinc-400">
Create your account to manage your health journey
</p>
</div>
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-zinc-800">
<ShieldCheck className="h-5 w-5 text-zinc-300" />
</div>
<div>
<h3 className="font-semibold">Private & Secure</h3>
<p className="text-sm text-zinc-400">
Your health data is encrypted and protected at all times
</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-zinc-800">
<HeartPulse className="h-5 w-5 text-zinc-300" />
</div>
<div>
<h3 className="font-semibold">Complete Health Management</h3>
<p className="text-sm text-zinc-400">
Appointments, records, prescriptions — all in one place
</p>
</div>
</div>
</div>
<p className="text-sm text-zinc-500">
© {new Date().getFullYear()} Desk. All rights reserved.
</p>
</div>
{/* Right Panel - Step Form */}
<div className="flex w-full lg:w-1/2 items-center justify-center bg-white p-6 sm:p-10">
<div className="w-full max-w-md space-y-6">
{/* Header */}
<div>
<h2 className="text-2xl font-bold text-zinc-900">{STEPS[step].title}</h2>
<p className="mt-1 text-sm text-zinc-500">{STEPS[step].description}</p>
</div>
{/* Step Indicator */}
<div className="flex items-center gap-2">
{STEPS.map((_, i) => (
<div
key={i}
className={cn(
"h-1.5 flex-1 rounded-full transition-colors",
i <= step ? "bg-zinc-900" : "bg-zinc-200"
)}
/>
))}
</div>
<p className="text-xs text-zinc-400">
Step {step + 1} of {STEPS.length}
</p>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Step 1: Personal Info */}
{step === 0 && (
<>
<div className="space-y-2">
<Label htmlFor="fullName" className="text-zinc-700">Full Name</Label>
<Input
id="fullName"
placeholder="John Doe"
className={inputClass}
aria-invalid={!!errors.fullName}
{...register("fullName")}
/>
{errors.fullName && (
<p className="text-sm text-destructive">{errors.fullName.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="gender" className="text-zinc-700">Gender</Label>
<NativeSelect
id="gender"
className={selectClass}
aria-invalid={!!errors.gender}
{...register("gender")}
>
<option value="">Select gender</option>
<option value="male">Male</option>
<option value="female">Female</option>
<option value="other">Other</option>
</NativeSelect>
{errors.gender && (
<p className="text-sm text-destructive">{errors.gender.message}</p>
)}
</div>
<div className="space-y-2">
<Label className="text-zinc-700">Date of Birth</Label>
<Popover open={dobOpen} onOpenChange={setDobOpen}>
<PopoverTrigger
className={cn(
"flex h-11 w-full items-center justify-start gap-2 rounded-lg border border-zinc-300 bg-white px-3 text-sm",
"focus-visible:border-zinc-900 focus-visible:ring-3 focus-visible:ring-zinc-900/20 focus-visible:outline-none",
!dobValue && "text-zinc-400"
)}
>
<CalendarIcon className="size-4" />
{dobValue ? format(new Date(dobValue), "PPP") : "Pick a date"}
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
mode="single"
selected={dobValue ? new Date(dobValue) : undefined}
onSelect={(date) => {
setValue(
"dateOfBirth",
date ? format(date, "yyyy-MM-dd") : "",
{ shouldValidate: true }
)
setDobOpen(false)
}}
captionLayout="dropdown"
defaultMonth={new Date(1990, 0)}
disabled={{ after: new Date() }}
/>
</PopoverContent>
</Popover>
{errors.dateOfBirth && (
<p className="text-sm text-destructive">{errors.dateOfBirth.message}</p>
)}
</div>
</>
)}
{/* Step 2: Contact Details */}
{step === 1 && (
<>
<div className="space-y-2">
<Label htmlFor="phone" className="text-zinc-700">Mobile Number</Label>
<Input
id="phone"
type="tel"
placeholder="+91 9876543210"
className={inputClass}
aria-invalid={!!errors.phone}
{...register("phone")}
/>
{errors.phone && (
<p className="text-sm text-destructive">{errors.phone.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-zinc-700">Email (optional)</Label>
<Input
id="email"
type="email"
placeholder="patient@example.com"
autoComplete="email"
className={inputClass}
aria-invalid={!!errors.email}
{...register("email")}
/>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>
</>
)}
{/* Step 3: Security & Preferences */}
{step === 2 && (
<>
<div className="space-y-2">
<Label htmlFor="password" className="text-zinc-700">Password</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
autoComplete="new-password"
className={cn(inputClass, "pr-10")}
aria-invalid={!!errors.password}
{...register("password")}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600"
tabIndex={-1}
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
{errors.password && (
<p className="text-sm text-destructive">{errors.password.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-zinc-700">Confirm Password</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="••••••••"
autoComplete="new-password"
className={cn(inputClass, "pr-10")}
aria-invalid={!!errors.confirmPassword}
{...register("confirmPassword")}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600"
tabIndex={-1}
aria-label={showConfirmPassword ? "Hide password" : "Show password"}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
{errors.confirmPassword && (
<p className="text-sm text-destructive">{errors.confirmPassword.message}</p>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="bloodGroup" className="text-zinc-700">Blood Group</Label>
<NativeSelect id="bloodGroup" className={selectClass} {...register("bloodGroup")}>
<option value="">Select (optional)</option>
<option value="A+">A+</option>
<option value="A-">A-</option>
<option value="B+">B+</option>
<option value="B-">B-</option>
<option value="AB+">AB+</option>
<option value="AB-">AB-</option>
<option value="O+">O+</option>
<option value="O-">O-</option>
</NativeSelect>
</div>
<div className="space-y-2">
<Label htmlFor="city" className="text-zinc-700">City</Label>
<Input
id="city"
placeholder="Chennai"
className={inputClass}
{...register("city")}
/>
</div>
</div>
</>
)}
{/* Navigation Buttons */}
<div className="flex gap-3 pt-2">
{step > 0 && (
<Button
type="button"
variant="outline"
size="lg"
className="h-11 flex-1 rounded-lg border-zinc-300 text-zinc-700 hover:bg-zinc-50"
onClick={handleBack}
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
)}
{step < STEPS.length - 1 ? (
<Button
type="button"
size="lg"
className="h-11 flex-1 rounded-lg bg-zinc-900 text-white hover:bg-zinc-800"
onClick={handleNext}
>
Continue
<ArrowRight className="h-4 w-4" />
</Button>
) : (
<Button
type="submit"
size="lg"
className="h-11 flex-1 rounded-lg bg-zinc-900 text-white hover:bg-zinc-800"
disabled={isSubmitting}
>
{isSubmitting && <Loader2 className="animate-spin" />}
{isSubmitting ? "Creating account..." : "Create Account"}
</Button>
)}
</div>
</form>
<p className="text-sm text-zinc-500 text-center">
Already have an account?{" "}
<Link to="/auth/login" className="text-zinc-900 font-medium hover:underline">
Sign in
</Link>
</p>
</div>
</div>
</div>
)
}
Example output

About the author
Technology Enthusiast and voracious reader with a demonstrated history of working in the computer software industry. Skilled in PHP, JavaScript, NodeJS, Angular, MySQL, MongoDB, Web3, Product Development, Project Management, and Teamwork.
Comments
Join the discussionNo comments yet. Be the first to comment.