You Might Also Like
Signup Form with Validation — HTML CSS JS Snippet
Signup Form · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Signup Form — Reward-Early/Punish-Late Validation, Password Strength Meter, Loading Button & Success Overlay

The signup form is the highest-stakes form on any product: every validation annoyance, unclear error, or premature red border costs real conversions. This snippet is a complete registration flow in vanilla HTML, CSS, and JavaScript that implements the patterns conversion research actually supports — blur-triggered validation that turns live only after a field has erred, a five-level password strength meter, show/hide password, a terms checkbox, a loading submit button, social sign-up buttons, and an animated "check your inbox" success overlay.
The validation timing that doesn't fight the user
When to show errors is the defining UX decision of any form, and this snippet implements the researched best practice known as *reward early, punish late*. Each field validates on blur — never while the user is still typing their email for the first time, which is why naive on-input validation feels hostile (you're told "invalid email" after typing one character). But once a field has erred, it flips into live mode: the touched map records that the field has been validated, and subsequent input events re-validate immediately — so the moment the user fixes the mistake, the red border and message vanish, rewarding the correction without waiting for another blur. Valid fields get a subtle green border only when non-empty. On submit, every field is force-touched and validated, and focus moves to the *first* failing input — the accessibility affordance most forms omit.
Validators as data, errors as paired elements
Validation logic is a plain object mapping field names to functions returning an error string or empty — adding a field means one entry and one markup block, no restructuring. Each field's error lives in a .su-error[data-for] paragraph that shows with a small drop-in animation; state classes (invalid/valid) live on the input and drive the border and focus-ring colours. The email regex is deliberately pragmatic (something@something.tld) — exhaustive RFC 5322 regexes reject real addresses and the confirmation email is the true validator anyway, which is exactly what the success screen communicates.
The strength meter: score, don't gatekeep
scorePassword() awards points for length ≥8, length ≥12, mixed case, digits, and symbols, mapping to five meter levels — width, colour (red→orange→yellow→lime→green), and an instructive label ("Weak — add numbers or symbols") that tells users *how* to improve rather than just judging them. Critically, the meter is advisory: only the 8-character minimum blocks submission. Hard composition rules (mandatory symbol + uppercase + digit) are an outdated NIST anti-pattern that produces "Password1!" — length-weighted scoring with guidance produces genuinely stronger passwords. The bar animates via width/background transitions; the show/hide toggle swaps input.type between password and text while updating its aria-label.
Submit, loading, and the success overlay
Submission disables the button, swaps its label to "Creating account…", and reveals a CSS border-spinner — the loading treatment that prevents double-submits without layout shift, since the spinner slots into the existing flex gap. The simulated API resolves into a success overlay: absolutely positioned over the whole card (the form's overflow: hidden keeps its rounded corners), fading in with a spring-popped green ring and echoing the submitted email in "We sent a confirmation link to…" — confirmation-email UX that closes the loop and tells users the next action. Social buttons (Google's official four-colour mark, GitHub's octocat path) sit under an ::before/::after line divider, and autocomplete attributes (name, email, new-password) are set so password managers behave — new-password specifically triggers suggested-password generation in Chrome and Safari.
Build with AI
Build, Understand, Optimize, and Extend It With AI
A signup form is mostly invisible decisions, and an AI assistant is good at making them visible: paste this snippet into Claude and ask it to enumerate every UX decision the code embodies — blur-then-live timing, advisory-not-blocking meter, pragmatic email regex, focus-first-failure — and the research reasoning behind each, so you can defend or adjust them for your product. Then make it yours: ask it to add the fields your funnel needs (company, role, phone with the right autocomplete and inputmode attributes) following the three-map pattern, to write the fetch integration with server-error mapping through showError including the "already exists → sign in with email prefilled" recovery path, and to add the HaveIBeenPwned k-anonymity breach check on password blur. If you're on React or Angular, ask for the conversion and — more valuable — ask it to show how this hand-rolled machine maps onto React Hook Form's onTouched mode or Angular's updateOn: "blur", so you understand exactly what the library abstracts before you adopt it.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a complete signup form in plain HTML, CSS, and JavaScript with production-grade validation UX — no libraries.
Requirements:
- Fields: full name, work email, password, and a terms-of-service checkbox, each field block containing a label, input with proper autocomplete attributes (name, email, new-password), and a paired error paragraph targeted by a data attribute; plus a gradient submit button, an "or sign up with" divider using ::before/::after lines, Google and GitHub social buttons with inline SVG marks, and a sign-in link.
- Implement reward-early/punish-late validation timing with three plain-object maps (fields, validators returning error-string-or-empty, touched): validate a field on blur; once a field has erred, re-validate it live on every input so fixes clear instantly; on submit, force-touch and validate everything and move focus to the FIRST invalid field.
- State classes on inputs (invalid red border, valid green border only when non-empty) with matching tinted focus rings, and error messages that animate in with a small drop-and-fade.
- A five-level password strength meter under the password field: score length ≥8, length ≥12, mixed case, digits, symbols; animate the bar's width and colour through red→orange→yellow→lime→green with instructive labels ("Weak — add numbers or symbols"); the meter is advisory — only the 8-character minimum blocks submission (comment why composition rules are a NIST anti-pattern).
- A show/hide password eye toggle that swaps input.type and keeps its aria-label in sync.
- On valid submit: disable the button, swap its label to "Creating account…", reveal a CSS border-spinner in the button's flex gap (no layout shift), and after a simulated 1.4s API call show a success overlay covering the card — fading in with a spring-scaled green check ring and "We sent a confirmation link to <the submitted email>".
- Use a pragmatic email regex (local@domain.tld) and comment that the confirmation email is the true validator; comment where server-side errors like "email already exists" would map back through the same error channel.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Test the validation behaviourTab through fields without typing — nothing turns red until you leave a field (blur). Enter a bad email and tab away: the error appears; now fix it and watch the error clear the instant the address becomes valid, without waiting for another blur. Type a password and watch the meter climb through five colour levels with instructive labels. Submit with gaps: every error shows and focus jumps to the first invalid field.
- 2Complete the happy pathFill valid values, tick the terms checkbox, and submit. The button disables, shows "Creating account…" with a spinner for 1.4s (the simulated API), then the success overlay pops in with the green ring and your email echoed in the confirmation message. The show/hide eye toggle reveals the password at any point.
- 3Wire it to your real backendIn the submit handler, replace the setTimeout with: const res = await fetch("/api/signup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: fields.name.value, email: fields.email.value, password: fields.password.value }) }). On non-OK responses, map server errors back through showError() — e.g. showError("email", "An account with this email already exists") — and re-enable the button. Never validate only client-side; this layer is UX, the server is truth.
- 4Add or remove fieldsEach field is one markup block (label + input + error paragraph with matching data-for) plus one entry in the fields map and one validator function. To add a company field: copy the name block, register fields.company, and add company: v => v.trim() ? "" : "Enter your company name". The blur/live wiring loops over the maps, so new fields inherit the full behaviour automatically.
- 5Tune the password policyThe blocking rule is the validator (8+ chars); the meter is advice. Adjust scorePassword's thresholds and the LEVELS labels/colours to your policy — but resist mandatory-composition rules; length-based scoring with guidance is the modern NIST-aligned approach. For breach checking, call the k-anonymity HaveIBeenPwned range API on blur and surface it as a meter label ("This password appeared in a breach — choose another").
- 6Export and composeClick JSX for React — port the fields/validators maps as-is, hold touched and errors in state, and validate in onBlur/onChange handlers. Pair with the Password Requirements Checklist for explicit rule display, OTP Verification for the post-signup email code, Magic Link Login and Social Login Buttons for alternative auth, and Glassmorphism Login for the sign-in side.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The two naive timings both fail users. Pure on-input validation punishes people mid-thought — "invalid email" appears after the first character and screams at them for the next ten keystrokes while they were never wrong, just unfinished. Pure on-submit validation hides all problems until the end, then dumps every error at once, forcing users to re-orient to fields they mentally closed. The blur-then-live hybrid (formalised in UX research as "reward early, punish late") gives errors only when a field is plausibly finished (blur), then switches that field to live mode so fixes are acknowledged instantly — the reward half, which is what makes correction feel responsive rather than nagging. The touched map is the whole mechanism: three booleans' worth of state per field. On submit, force-touching everything ensures untouched-but-required fields still report, and focusing the first invalid field gives keyboard and screen-reader users a direct path to the problem.
Both are deliberate, standards-aligned choices. NIST SP 800-63B explicitly recommends against composition rules (mandatory symbol/uppercase/digit) because they produce predictable patterns ("Password1!") while blocking genuinely strong long passphrases; the guidance is a length minimum, a breach-list check, and user education — which is exactly this form: 8 characters blocks, the meter educates with instructive labels, and the how-to shows where the HaveIBeenPwned k-anonymity check slots in. The email regex (something@something.tld) is similarly pragmatic: fully RFC-compliant regexes are famous for rejecting valid real-world addresses (plus-tags, newer TLDs, quoted locals), and no client regex can confirm deliverability anyway. The confirmation email is the actual validator — the regex's only job is catching obvious typos like a missing @ before the round-trip, and the success screen's "check your inbox" makes that contract explicit.
Reuse the exact same channel as client errors so the user sees one consistent system. After your fetch, branch on the response: a 409 or a validation payload maps field-by-field through showError() — showError("email", "An account with this email already exists — try signing in") — then re-enable the button, restore its label, hide the spinner, and focus the offending field. For non-field errors (rate limits, outages), add one form-level error element above the submit button styled like .su-error. Two details worth copying from production forms: mark the server-erred field as touched so it re-validates live as the user edits (the map already handles this), and make the "already exists" message link to sign-in with the email prefilled — that error is your highest-intent recovery path, not a dead end.
React: the three maps become state — const [touched, setTouched] and const [errors, setErrors] — with validators kept verbatim as a plain object outside the component; bind onBlur={() => touch(name)} and onChange, derive input classes from errors[name], and you have hand-rolled the core of React Hook Form (whose mode: "onTouched" is literally this timing; adopt it when forms multiply). Angular: Reactive Forms encode the same machine natively — updateOn: "blur" on the FormControl gives the first-pass timing, and markAllAsTouched() on submit replaces the force-touch loop; the meter becomes a pipe over the password control's valueChanges. Tailwind: inputs are w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 with state variants data-[invalid]:border-red-400 data-[valid]:border-emerald-400; the meter bar is h-1 rounded transition-all with width/colour set inline from the level; the spinner is size-4 rounded-full border-2 border-white/35 border-t-white animate-spin.