niceui

Sign Up Form

A sign-up card with social buttons, inline validation, a live password strength meter and an animated success state.

ReactTailwind CSSTypeScriptBlocksMIT · Free
Live preview

Installation

01

Install with the shadcn CLI. The NiceUI theme comes along the first time.

$ npx shadcn@latest add https://niceui.dev/r/blocks-sign-up-form.json
02

Or copy the source below. Dependencies: lucide-react.

Source

components/niceui/blocks/SignUpForm.tsx
import { useState, type FormEvent } from 'react';
import { ArrowRight, Check, Eye, EyeOff, Github, Loader2, Lock, Mail, Twitter } from 'lucide-react';
import { cn } from '@/lib/cn';

export function passwordScore(pw: string) {
  const checks = [pw.length >= 8, /[A-Z]/.test(pw) && /[a-z]/.test(pw), /\d/.test(pw), /[^A-Za-z0-9]/.test(pw)];
  return { checks, score: checks.filter(Boolean).length };
}

const LEVELS = [
  { label: 'Too weak', color: 'bg-rose-500', text: 'text-rose-400' },
  { label: 'Weak', color: 'bg-rose-500', text: 'text-rose-400' },
  { label: 'Fair', color: 'bg-amber-400', text: 'text-amber-400' },
  { label: 'Good', color: 'bg-sky-400', text: 'text-sky-400' },
  { label: 'Strong', color: 'bg-emerald-400', text: 'text-emerald-400' },
];
const RULES = ['8+ characters', 'Upper & lowercase', 'A number', 'A symbol'];

export type SignUpFormProps = { onSubmit?: (data: { email: string; password: string }) => Promise<void> };

export function SignUpForm({ onSubmit = () => new Promise((r) => setTimeout(r, 1300)) }: SignUpFormProps) {
  const [email, setEmail] = useState('');
  const [pw, setPw] = useState('');
  const [show, setShow] = useState(false);
  const [touched, setTouched] = useState({ email: false, pw: false });
  const [state, setState] = useState<'idle' | 'loading' | 'done'>('idle');
  const [shake, setShake] = useState(0);
  const { checks, score } = passwordScore(pw);
  const emailErr = !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? (email ? 'Enter a valid email address' : 'Email is required') : null;
  const pwErr = score < 3 ? 'Use a stronger password' : null;
  const level = LEVELS[pw ? score : 0];

  const submit = async (e: FormEvent) => {
    e.preventDefault();
    setTouched({ email: true, pw: true });
    if (emailErr || pwErr) return setShake((s) => s + 1);
    setState('loading');
    await onSubmit({ email, password: pw });
    setState('done');
  };

  if (state === 'done') {
    return (
      <div className="flex w-full max-w-sm animate-scale-in flex-col items-center rounded-2xl bg-ink-900 px-6 py-10 text-center shadow-card">
        <span className="relative flex h-14 w-14 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-400">
          <span className="absolute inset-0 animate-pulse-ring rounded-full bg-emerald-500/30" />
          <Check className="relative h-6 w-6 animate-scale-in" />
        </span>
        <p className="mt-5 text-lg font-medium text-ink-50">Check your inbox</p>
        <p className="mt-1 text-sm text-ink-400">We sent a confirmation link to <span className="text-ink-200">{email}</span></p>
        <button onClick={() => { setState('idle'); setPw(''); setTouched({ email: false, pw: false }); }} className="mt-6 text-xs text-ink-400 transition-colors hover:text-ink-100">Use a different email</button>
      </div>
    );
  }

  const field = 'flex h-11 items-center gap-2.5 rounded-xl bg-ink-850 px-3 transition-shadow duration-200 focus-within:shadow-glow';
  return (
    <form onSubmit={submit} noValidate className="w-full max-w-sm rounded-2xl bg-ink-900 p-6 shadow-card">
      <p className="text-lg font-medium text-ink-50">Create your account</p>
      <p className="mt-1 text-sm text-ink-400">Start building with NiceUI in seconds.</p>
      <div className="mt-5 grid grid-cols-2 gap-2">
        {[{ icon: Github, label: 'GitHub' }, { icon: Twitter, label: 'Twitter' }].map(({ icon: Icon, label }) => (
          <button key={label} type="button" className="focus-ring flex h-10 items-center justify-center gap-2 rounded-xl bg-ink-850 text-sm font-medium text-ink-100 shadow-hairline transition-all hover:bg-ink-800 active:scale-[0.97]">
            <Icon className="h-4 w-4" /> {label}
          </button>
        ))}
      </div>
      <div className="my-5 flex items-center gap-3 text-[11px] uppercase tracking-wider text-ink-500">
        <span className="h-px flex-1 bg-white/5" /> or <span className="h-px flex-1 bg-white/5" />
      </div>
      <div key={shake} className={cn('space-y-3', shake > 0 && 'animate-wiggle')}>
        <div>
          <label className={cn(field, touched.email && emailErr ? 'shadow-[0_0_0_1px_rgba(251,113,133,0.5)]' : 'shadow-hairline')}>
            <Mail className="h-4 w-4 text-ink-500" />
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} onBlur={() => setTouched((t) => ({ ...t, email: true }))} placeholder="you@company.com" className="h-full flex-1 bg-transparent text-sm text-ink-50 outline-none placeholder:text-ink-500" />
          </label>
          <p className={cn('overflow-hidden text-xs text-rose-400 transition-all duration-300', touched.email && emailErr ? 'mt-1.5 max-h-5 opacity-100' : 'max-h-0 opacity-0')}>{emailErr}</p>
        </div>
        <div>
          <label className={cn(field, touched.pw && pwErr ? 'shadow-[0_0_0_1px_rgba(251,113,133,0.5)]' : 'shadow-hairline')}>
            <Lock className="h-4 w-4 text-ink-500" />
            <input type={show ? 'text' : 'password'} value={pw} onChange={(e) => setPw(e.target.value)} onBlur={() => setTouched((t) => ({ ...t, pw: true }))} placeholder="Password" className="h-full flex-1 bg-transparent text-sm text-ink-50 outline-none placeholder:text-ink-500" />
            <button type="button" aria-label={show ? 'Hide password' : 'Show password'} onClick={() => setShow((s) => !s)} className="text-ink-500 transition-colors hover:text-ink-200">
              {show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
            </button>
          </label>
          <div className={cn('grid transition-all duration-500 ease-smooth', pw ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
            <div className="overflow-hidden">
              <div className="mt-2.5 flex items-center gap-2">
                <div className="flex flex-1 gap-1">
                  {[0, 1, 2, 3].map((i) => (
                    <span key={i} className="h-1 flex-1 overflow-hidden rounded-full bg-ink-800">
                      <span className={cn('block h-full origin-left rounded-full transition-all duration-500 ease-spring', level.color, i < score ? 'scale-x-100' : 'scale-x-0')} style={{ transitionDelay: `${i * 40}ms` }} />
                    </span>
                  ))}
                </div>
                <span className={cn('w-14 text-right text-[11px] font-medium transition-colors', level.text)}>{level.label}</span>
              </div>
              <div className="mt-2 grid grid-cols-2 gap-1">
                {RULES.map((r, i) => (
                  <span key={r} className={cn('flex items-center gap-1.5 text-[11px] transition-colors duration-300', checks[i] ? 'text-ink-200' : 'text-ink-500')}>
                    <span className={cn('flex h-3.5 w-3.5 items-center justify-center rounded-full transition-all duration-300', checks[i] ? 'bg-emerald-500 text-ink-950' : 'bg-ink-800')}>
                      <Check className={cn('h-2.5 w-2.5 transition-transform duration-300 ease-spring', checks[i] ? 'scale-100' : 'scale-0')} />
                    </span>
                    {r}
                  </span>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
      <button type="submit" disabled={state === 'loading'} className="focus-ring group mt-5 flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-ink-50 text-sm font-medium text-ink-950 transition-all hover:bg-white active:scale-[0.98] disabled:opacity-70">
        {state === 'loading' ? <><Loader2 className="h-4 w-4 animate-spin" /> Creating account…</> : <>Create account <ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" /></>}
      </button>
      <p className="mt-4 text-center text-xs text-ink-500">By continuing you agree to our Terms and Privacy Policy.</p>
    </form>
  );
}

export function SignUpFormDemo() {
  return (
    <div className="flex justify-center">
      <SignUpForm />
    </div>
  );
}