Signup Form
Original · freesignup form with password strength
byWeb InnoventixReact + Tailwind
formsignupforms
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/form-signup.jsonform-signup.tsx
"use client";
import { useId, useMemo, useState } from "react";
import type { FormEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Requirement = {
id: "length" | "lower" | "upper" | "number" | "symbol";
label: string;
test: (value: string) => boolean;
};
type StrengthLevel = {
label: string;
bar: string;
text: string;
};
const REQUIREMENTS: Requirement[] = [
{ id: "length", label: "At least 8 characters", test: (v) => v.length >= 8 },
{ id: "lower", label: "One lowercase letter", test: (v) => /[a-z]/.test(v) },
{ id: "upper", label: "One uppercase letter", test: (v) => /[A-Z]/.test(v) },
{ id: "number", label: "One number", test: (v) => /[0-9]/.test(v) },
{ id: "symbol", label: "One symbol (!@#$…)", test: (v) => /[^A-Za-z0-9]/.test(v) },
];
const LEVELS: StrengthLevel[] = [
{ label: "Enter a password", bar: "bg-slate-300 dark:bg-slate-700", text: "text-slate-400 dark:text-slate-500" },
{ label: "Too weak", bar: "bg-rose-500", text: "text-rose-600 dark:text-rose-400" },
{ label: "Weak", bar: "bg-amber-500", text: "text-amber-600 dark:text-amber-400" },
{ label: "Good", bar: "bg-sky-500", text: "text-sky-600 dark:text-sky-400" },
{ label: "Strong", bar: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400" },
];
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function scorePassword(value: string, met: number): number {
if (!value) return 0;
let s = 0;
if (value.length >= 8) s += 1;
if (met >= 3) s += 1;
if (met >= 4) s += 1;
if (value.length >= 12 && met >= 5) s += 1;
return Math.min(Math.max(s, value ? 1 : 0), 4);
}
function EyeIcon({ open }: { open: boolean }) {
return open ? (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
<circle cx="12" cy="12" r="3" />
</svg>
) : (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
<path d="M4 4l16 16" />
<path d="M9.9 5.2A9.6 9.6 0 0 1 12 5c6 0 9.5 6.5 9.5 6.5a15.7 15.7 0 0 1-2.9 3.6M6.4 7.1A15.6 15.6 0 0 0 2.5 11.5S6 18 12 18a9 9 0 0 0 3.3-.6" />
<path d="M9.9 10a3 3 0 0 0 4.2 4.2" />
</svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3" aria-hidden="true">
<path d="M4 10.5l3.5 3.5L16 5.5" />
</svg>
);
}
function DotIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" className="h-3 w-3" aria-hidden="true">
<path d="M6 10h8" />
</svg>
);
}
export default function FormSignup() {
const reduce = useReducedMotion();
const uid = useId();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [agree, setAgree] = useState(false);
const [show, setShow] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [pending, setPending] = useState(false);
const [done, setDone] = useState(false);
const metFlags = useMemo(
() => REQUIREMENTS.map((r) => r.test(password)),
[password],
);
const metCount = metFlags.filter(Boolean).length;
const score = useMemo(() => scorePassword(password, metCount), [password, metCount]);
const level = LEVELS[score];
const nameError = !name.trim() ? "Please enter your name." : "";
const emailError = !email
? "Please enter your email."
: !EMAIL_RE.test(email)
? "Enter a valid email address."
: "";
const passwordError = !password
? "Please create a password."
: score < 2
? "Your password is too weak."
: "";
const confirmError = !confirm
? "Please confirm your password."
: confirm !== password
? "Passwords don't match."
: "";
const agreeError = !agree ? "You must accept the terms to continue." : "";
const formValid = !nameError && !emailError && !passwordError && !confirmError && !agreeError;
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setSubmitted(true);
if (!formValid || pending) return;
setPending(true);
window.setTimeout(() => {
setPending(false);
setDone(true);
}, 1100);
}
const baseInput =
"w-full rounded-xl border bg-white px-3.5 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-900/70 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-950";
function inputBorder(hasError: boolean): string {
return hasError
? "border-rose-400 dark:border-rose-500/70"
: "border-slate-300 hover:border-slate-400 dark:border-slate-700 dark:hover:border-slate-600";
}
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 sm:px-6 sm:py-24 dark:bg-slate-950">
<style>{`
@keyframes fsig-rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fsig-shimmer {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes fsig-pop {
0% { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.12); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fsig-draw {
from { stroke-dashoffset: 32; }
to { stroke-dashoffset: 0; }
}
.fsig-rise { animation: fsig-rise 0.5s cubic-bezier(0.22,1,0.36,1) both; }
.fsig-shimmer {
background-size: 200% 100%;
animation: fsig-shimmer 2.4s linear infinite;
}
.fsig-pop { animation: fsig-pop 0.45s cubic-bezier(0.22,1,0.36,1) both; }
.fsig-check-path {
stroke-dasharray: 32;
animation: fsig-draw 0.5s 0.15s ease-out both;
}
@media (prefers-reduced-motion: reduce) {
.fsig-rise, .fsig-shimmer, .fsig-pop, .fsig-check-path {
animation: none !important;
}
}
`}</style>
<div className="relative mx-auto w-full max-w-md">
<div className="pointer-events-none absolute -inset-px rounded-3xl bg-gradient-to-b from-indigo-200/40 via-transparent to-violet-200/30 blur-2xl dark:from-indigo-500/10 dark:to-violet-500/10" aria-hidden="true" />
<div className={`relative rounded-3xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-900/5 sm:p-8 dark:border-slate-800 dark:bg-slate-900/80 dark:shadow-black/30 ${reduce ? "" : "fsig-rise"}`}>
<AnimatePresence mode="wait" initial={false}>
{done ? (
<motion.div
key="success"
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? undefined : { opacity: 0, y: -8 }}
transition={{ duration: 0.35 }}
className="flex flex-col items-center py-6 text-center"
role="status"
aria-live="polite"
>
<span className={`mb-5 inline-flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400 ${reduce ? "" : "fsig-pop"}`}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" className="h-8 w-8" aria-hidden="true">
<path className={reduce ? "" : "fsig-check-path"} d="M5 12.5l4.5 4.5L19 7" />
</svg>
</span>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">You're all set</h2>
<p className="mt-2 max-w-xs text-sm text-slate-600 dark:text-slate-400">
We sent a confirmation link to <span className="font-medium text-slate-900 dark:text-slate-200">{email}</span>. Click it to activate your workspace.
</p>
<button
type="button"
onClick={() => {
setDone(false);
setSubmitted(false);
setName("");
setEmail("");
setPassword("");
setConfirm("");
setAgree(false);
}}
className="mt-6 rounded-xl px-4 py-2 text-sm font-medium text-indigo-600 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
Create another account
</button>
</motion.div>
) : (
<motion.div
key="form"
initial={false}
exit={reduce ? undefined : { opacity: 0 }}
>
<header className="mb-6">
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
14-day free trial · no card required
</div>
<h1 className="text-2xl font-semibold tracking-tight text-slate-900 dark:text-white">
Create your Meridian account
</h1>
<p className="mt-1.5 text-sm text-slate-600 dark:text-slate-400">
Ship projects faster with your whole team in one place.
</p>
</header>
<form onSubmit={handleSubmit} noValidate className="space-y-4">
{/* Name */}
<div>
<label htmlFor={`${uid}-name`} className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-300">
Full name
</label>
<input
id={`${uid}-name`}
name="name"
type="text"
autoComplete="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Ada Lovelace"
aria-invalid={submitted && !!nameError}
aria-describedby={submitted && nameError ? `${uid}-name-err` : undefined}
className={`${baseInput} ${inputBorder(submitted && !!nameError)}`}
/>
{submitted && nameError ? (
<p id={`${uid}-name-err`} role="alert" className="mt-1.5 text-xs text-rose-600 dark:text-rose-400">
{nameError}
</p>
) : null}
</div>
{/* Email */}
<div>
<label htmlFor={`${uid}-email`} className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-300">
Work email
</label>
<input
id={`${uid}-email`}
name="email"
type="email"
autoComplete="email"
inputMode="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="ada@company.com"
aria-invalid={submitted && !!emailError}
aria-describedby={submitted && emailError ? `${uid}-email-err` : undefined}
className={`${baseInput} ${inputBorder(submitted && !!emailError)}`}
/>
{submitted && emailError ? (
<p id={`${uid}-email-err`} role="alert" className="mt-1.5 text-xs text-rose-600 dark:text-rose-400">
{emailError}
</p>
) : null}
</div>
{/* Password */}
<div>
<label htmlFor={`${uid}-password`} className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-300">
Password
</label>
<div className="relative">
<input
id={`${uid}-password`}
name="password"
type={show ? "text" : "password"}
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Create a strong password"
aria-invalid={submitted && !!passwordError}
aria-describedby={`${uid}-strength ${uid}-reqs${submitted && passwordError ? ` ${uid}-password-err` : ""}`}
className={`${baseInput} pr-11 ${inputBorder(submitted && !!passwordError)}`}
/>
<button
type="button"
onClick={() => setShow((s) => !s)}
aria-pressed={show}
aria-label={show ? "Hide password" : "Show password"}
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-xl text-slate-500 transition-colors hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900"
>
<EyeIcon open={show} />
</button>
</div>
{/* Strength meter */}
<div className="mt-3">
<div className="flex gap-1.5" aria-hidden="true">
{[0, 1, 2, 3].map((i) => {
const filled = i < score;
const active = i === score - 1;
return (
<span
key={i}
className={`h-1.5 flex-1 rounded-full transition-colors duration-300 ${
filled ? level.bar : "bg-slate-200 dark:bg-slate-700"
} ${filled && active && !reduce ? "fsig-shimmer" : ""}`}
/>
);
})}
</div>
<p
id={`${uid}-strength`}
role="status"
aria-live="polite"
className={`mt-1.5 text-xs font-medium ${level.text}`}
>
Password strength: {level.label}
</p>
</div>
{/* Requirements */}
<ul id={`${uid}-reqs`} className="mt-3 grid grid-cols-1 gap-1.5 sm:grid-cols-2">
{REQUIREMENTS.map((req, i) => {
const ok = metFlags[i];
return (
<li key={req.id} className="flex items-center gap-2 text-xs">
<span
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full transition-colors ${
ok
? "bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400"
: "bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500"
}`}
aria-hidden="true"
>
{ok ? <CheckIcon /> : <DotIcon />}
</span>
<span className={ok ? "text-slate-700 dark:text-slate-300" : "text-slate-500 dark:text-slate-500"}>
{req.label}
</span>
<span className="sr-only">{ok ? "met" : "not met"}</span>
</li>
);
})}
</ul>
{submitted && passwordError ? (
<p id={`${uid}-password-err`} role="alert" className="mt-2 text-xs text-rose-600 dark:text-rose-400">
{passwordError}
</p>
) : null}
</div>
{/* Confirm password */}
<div>
<label htmlFor={`${uid}-confirm`} className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-300">
Confirm password
</label>
<div className="relative">
<input
id={`${uid}-confirm`}
name="confirm"
type={showConfirm ? "text" : "password"}
autoComplete="new-password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
placeholder="Re-enter your password"
aria-invalid={submitted && !!confirmError}
aria-describedby={submitted && confirmError ? `${uid}-confirm-err` : undefined}
className={`${baseInput} pr-11 ${inputBorder(submitted && !!confirmError)}`}
/>
<button
type="button"
onClick={() => setShowConfirm((s) => !s)}
aria-pressed={showConfirm}
aria-label={showConfirm ? "Hide confirmation password" : "Show confirmation password"}
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-xl text-slate-500 transition-colors hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900"
>
<EyeIcon open={showConfirm} />
</button>
</div>
{submitted && confirmError ? (
<p id={`${uid}-confirm-err`} role="alert" className="mt-1.5 text-xs text-rose-600 dark:text-rose-400">
{confirmError}
</p>
) : null}
</div>
{/* Terms */}
<div>
<label htmlFor={`${uid}-agree`} className="flex cursor-pointer items-start gap-2.5 text-sm text-slate-600 dark:text-slate-400">
<input
id={`${uid}-agree`}
name="agree"
type="checkbox"
checked={agree}
onChange={(e) => setAgree(e.target.checked)}
aria-invalid={submitted && !!agreeError}
aria-describedby={submitted && agreeError ? `${uid}-agree-err` : undefined}
className="mt-0.5 h-4 w-4 shrink-0 rounded border-slate-300 text-indigo-600 accent-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-600 dark:focus-visible:ring-offset-slate-900"
/>
<span>
I agree to the{" "}
<a
href="#terms"
className="font-medium text-indigo-600 underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
Terms of Service
</a>{" "}
and{" "}
<a
href="#privacy"
className="font-medium text-indigo-600 underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
Privacy Policy
</a>
.
</span>
</label>
{submitted && agreeError ? (
<p id={`${uid}-agree-err`} role="alert" className="mt-1.5 text-xs text-rose-600 dark:text-rose-400">
{agreeError}
</p>
) : null}
</div>
{/* Submit */}
<motion.button
type="submit"
disabled={pending}
whileTap={reduce ? undefined : { scale: 0.985 }}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm shadow-indigo-600/20 transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-70 dark:focus-visible:ring-offset-slate-900"
>
{pending ? (
<>
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4 animate-spin" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth={3} className="opacity-25" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth={3} strokeLinecap="round" />
</svg>
Creating account…
</>
) : (
"Create account"
)}
</motion.button>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
Already have an account?{" "}
<a
href="#login"
className="font-medium text-indigo-600 underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
>
Sign in
</a>
</p>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Login Form
Originalclean login form with social buttons

Contact Form
Originalcontact form with fields and message

Multistep Form
Originalmulti-step wizard form with progress

Payment Form
Originalcard payment form with formatting

Newsletter Form
Originalinline newsletter subscribe form

Search Filters Form
Originalfaceted search filter panel

Profile Form
Originalprofile settings form

OTP Verify Form
OriginalOTP verification form

Upload Dropzone Form
Originaldrag-and-drop file upload with preview

Feedback Form
Originalfeedback form with emoji rating

Survey Form
Originalshort survey with radios and scale

Booking Form
Originalbooking form with date and slots

