Password Reset Form — Free HTML CSS JS Forgot Password Flow Snippet

Password Reset Form · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Two-step flow — email entry and confirmation — built as toggled sibling divs in one component
Lightweight regex email validation with distinct messages for empty vs. malformed input
Confirmation screen echoes back the exact email address the user submitted
"Resend email" button with a temporary disabled state to prevent accidental double-sends
"Use a different email" link resets the form and returns to the first step cleanly
Invalid input gets both a red border and an inline text error for clear, accessible feedback
Backend call is left as a clearly marked placeholder, ready for your own reset endpoint
Visually-hidden label keeps the email input accessible without cluttering the visual design
Focus ring and disabled states styled for both mouse and keyboard interaction
No framework, no form validation library, no build step required

About this UI Snippet

Password Reset Form — HTML, CSS & JavaScript Forgot Password Flow

Screenshot of the Password Reset Form snippet rendered live

A password reset flow is really two screens compressed into one component: an email-collection step, and a confirmation step telling the user to check their inbox. Building both as a single toggled component — rather than two separate pages — keeps the transition instant and avoids an unnecessary page reload for what is fundamentally client-side state.

This snippet implements both steps in plain HTML, CSS, and vanilla JavaScript.

How the two-step structure works

Both steps exist in the DOM at all times as sibling .step divs, #stepRequest and #stepSent, each with display: none by default and display: block only when it carries the .active class. A single goToStep(stepId) helper removes .active from every step and adds it to just the target one — the same show-one-hide-rest pattern used in a tabbed interface, applied here to a linear flow instead of freely switchable tabs.

How email validation works

On submit, isValidEmail runs a lightweight regex (/^[^\s@]+@[^\s@]+\.[^\s@]+$/) that checks for the basic shape of an email address — something, an @, something, a dot, something — without trying to fully validate every RFC 5322 edge case, which is famously impractical with regex alone. An empty field and an invalid-but-non-empty field show two different messages, both surfaced in a dedicated .error-msg span with aria--friendly announcement in mind, and the input gets a .invalid class that turns its border red.

Why the request itself isn't shown here

The actual backend call (fetch('/api/password-reset', ...)) is left as a commented-out placeholder rather than wired to a real endpoint, since every backend's reset-token flow differs. The important client-side behavior — validating the email, transitioning to the confirmation screen, and remembering which address was submitted so it can be echoed back to the user — is fully implemented and ready to have the real network call dropped in.

Security note on the confirmation step

A real password-reset flow should show the same "check your email" confirmation screen whether or not the submitted email actually exists in your system. Never reveal whether an address is registered by branching the UI on that — it's a common account-enumeration vulnerability. This snippet's confirmation step is intentionally always the same regardless of what happens on your backend.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why the confirmation screen must always look identical regardless of whether the submitted email exists in your system, and to review your specific backend's password-reset endpoint design for account-enumeration risks. It's also a great snippet to extend with the assistant's help: ask it to add a loading spinner state on the submit button while the network request is in flight, to add a resend cooldown timer that persists across page reloads via localStorage, or to help wire the placeholder fetch call to a specific auth provider's SDK (Firebase, Auth0, Supabase, or a custom Express/Django backend) and handle its particular error responses gracefully in the existing error-message UI.

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 two-step "forgot password" form in plain HTML, CSS, and JavaScript — no framework, no form library.

Requirements:
- Two step containers that both exist in the DOM at all times, toggled between via a single shared active class and a small goToStep helper function — not two separate pages or a routing library.
- Step one: an email input with a visually-hidden but properly associated label, a submit button, and client-side validation on submit that checks for both an empty field and a malformed email address, each producing a distinct inline error message plus a visual invalid state on the input.
- Step two: a confirmation screen that echoes back the exact email address the user submitted, plus a "resend email" button that temporarily disables itself with different button text for a couple of seconds to prevent accidental double-sends, and a "use a different email" control that clears the form and returns to step one.
- Leave the actual network call to a backend password-reset endpoint as a clearly commented placeholder inside the submit handler rather than a real fetch call, since every backend's reset-token API differs.
- Ensure the confirmation step's content and behavior does not vary based on whether the submitted email is valid or registered in any real system — the same confirmation UI must show regardless, to avoid revealing which emails have accounts.

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
    Load the snippetClick "Password Reset Form" in the sidebar Library tab. The preview starts on the email-entry step.
  2. 2
    Try invalid inputClick "Send reset link" with an empty or malformed email to see the inline validation error appear.
  3. 3
    Submit a valid emailEnter a properly formatted email and submit — the form transitions to the "Check your email" confirmation step, echoing back the address you entered.
  4. 4
    Wire up the real requestIn the JS panel, uncomment and adapt the fetch() call inside the submit handler to call your actual password-reset API endpoint.
  5. 5
    Adjust the validation ruleIn the JS panel, edit the isValidEmail regex if you need stricter or more permissive email format checking.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for React, or "Tailwind" for React + Tailwind CSS.

Real-world uses

Common Use Cases

AUTH
Forgot-password flows for any auth system
Drop this in as the client-side UI for a password reset feature on top of any backend authentication provider.
Learn step-toggling without a router
Study how a multi-step flow can live entirely on one page using a single active-class toggle, with no client-side routing library involved.
Prototype an onboarding or account-recovery flow
Use this as a starting point in a prototype for any multi-step account flow — signup confirmation, email verification, or invite acceptance.
Match your auth screens' branding
Restyle the icons, colors, and card shape to match your product's existing login and signup screens.
Practice accessible form error patterns
The paired invalid class plus dedicated error text span is a reusable pattern for any form field needing inline validation feedback.
Wire up a real reset-token backend
Use the placeholder fetch call as the exact spot to integrate with your provider's password-reset API (Firebase Auth, Auth0, Supabase, or a custom backend).

Got questions?

Frequently Asked Questions

No — the network call is left as a commented-out placeholder fetch() inside the submit handler. You need to connect it to your own backend or auth provider's password-reset endpoint to actually send an email.

It uses a lightweight regex that checks for the general shape of an email address (text, an @ symbol, more text, a dot, more text). It intentionally does not attempt full RFC 5322 validation, which is impractical with regex — real validation ultimately happens server-side when the reset request is processed.

This is a deliberate security practice called avoiding account enumeration. If the UI revealed whether an email was registered, an attacker could use the reset form to discover which addresses have accounts. Always show the same confirmation message whether or not the email exists in your system.

It briefly disables itself and shows "Sent!" for two seconds to prevent accidental rapid double-clicks, then re-enables. You should wire this to actually re-trigger your backend's reset-email endpoint using the previously submitted address.

Click "Use a different email" on the confirmation screen — it clears the input and error state and returns you to the first step so a new address can be entered.

Yes. Disable the submit button and swap its text to something like "Sending..." at the start of the submit handler, then re-enable it (or transition to the confirmation step) once your fetch call resolves.

Yes. The input has an associated <label> that is visually hidden using a standard clip-based technique rather than display: none, so it remains in the accessibility tree and is announced by screen readers even though it is not visually shown.

Extend the resendEmail function to track a timestamp of the last request (e.g. in a variable or localStorage) and keep the button disabled with a countdown until a minimum interval has passed, in addition to your backend enforcing its own rate limit.