Signup Form with Validation — HTML CSS JS Snippet

Signup Form · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Reward-early/punish-late validation: blur-triggered first pass, live re-validation only after a field errs
Validators as a data map — one function per field returning an error string, trivially extensible
Five-level password strength meter with colour ramp and instructive labels; advisory, not gatekeeping
Show/hide password toggle swapping input.type with synced aria-label
Submit force-touches all fields and focuses the first invalid one
Loading button state: disabled, relabelled, CSS border-spinner — no layout shift, no double submits
Animated success overlay with spring-popped ring, echoing the submitted email
Correct autocomplete attributes (name/email/new-password) so password managers offer generation

About this UI Snippet

Signup Form — Reward-Early/Punish-Late Validation, Password Strength Meter, Loading Button & Success Overlay

Screenshot of the Signup Form snippet rendered live

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:

text
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

  1. 1
    Test 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.
  2. 2
    Complete 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.
  3. 3
    Wire 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.
  4. 4
    Add 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.
  5. 5
    Tune 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").
  6. 6
    Export 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

SaaS registration pages and trial funnels
This is the drop-in top of a SaaS funnel: trial-framed copy ("free 14-day trial, no credit card"), minimal required fields, social alternatives, and the check-your-inbox handoff to email confirmation. The validation timing directly protects conversion — premature red borders are a measured abandonment cause — and server-error mapping through showError() keeps "email already exists" in the same visual language as client errors. Wire the success overlay to your resend-email endpoint for the complete loop.
The reference implementation of form validation UX
Beyond signup, the touched-map pattern here is the correct answer to "when do I validate?" for every form you build: checkout, settings, onboarding. Blur first, live-after-error, force-touch on submit, focus the first failure. The snippet isolates that logic in ~20 lines you can lift wholesale — the fields map, validators map, touched map, and the three event bindings — and reuse under any styling. Compare with the Inline Validation Form for the same philosophy on a smaller form.
Password UX done to current standards
Security teams increasingly require NIST-aligned password UX: length-first scoring, no forced composition, breach checking, paste allowed, and manager-friendly autocomplete. This form ships that posture — the meter guides toward strength without blocking, new-password triggers browser password generation, and the show/hide toggle (with aria-label sync) reduces typos that drive reset volume. Add the HaveIBeenPwned range check from the how-to for the full modern stack.
Teaching form state machines
Each field here moves through a small state machine — pristine → touched → invalid ⇄ valid — and the snippet makes the transitions explicit and inspectable: the touched map is the state, blur/input events are the transitions, and classes/error elements are the render. That framing transfers directly to React Hook Form, Formik, or any form library, all of which formalise exactly this machine; students who build it manually once understand what those libraries' dirty/touched/error APIs actually track.
Waitlists, newsletters, and event registrations
Strip the password block and this becomes every capture form: waitlist signups (pair with the Waitlist Signup treatment), event registration, beta access. The parts that carry over unchanged — blur-then-live validation, loading button, success overlay echoing the email — are precisely what separates a credible capture form from a mailto link. The success overlay's "check your inbox" copy slots straight into double-opt-in newsletter flows.
Design-system form primitives on dark surfaces
The form demonstrates a complete dark-theme input system worth extracting into tokens: slate wells (#0f172a on #1e293b), indigo focus ring via 3px rgba box-shadow, red/green semantic borders with matching tinted rings, error text at 11.5px with a drop-in animation, and accent-color on the checkbox. Lift .su-input and its state classes into your design system and every form in the product inherits consistent validation visuals — the same input recipe used across this library's Contact Form and Checkout Form.

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.