Web InnoventixFreeCode

Signup Form

Original · free

signup form with password strength

byWeb InnoventixReact + Tailwind
formsignupforms
Open

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.json
form-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 quote

Similar components

Browse all →