Skip to content
AgileLogics
7 min read

Production-Grade Forms

Prerequisites: Better forms: hints, errors, and feedback, Sanitizing user content

Forms on real sites face attackers, bots, password managers, slow networks, and double clicks. The HTML side of handling them well:

CSRF tokens

Cross-site request forgery: another site makes a visitor's browser submit a form to your site, with the visitor's cookies attached. The standard defense is a secret token in every state-changing form, checked by the server:

<form action="/account/email" method="post">
  <input type="hidden" name="_token" value="c2f1e9a0b7d44f6e8a1d3b5c7e9f0a2b">
  <label for="new-email">New email</label>
  <input id="new-email" name="email" type="email" autocomplete="email" required>
  <button>Update email</button>
</form>

Frameworks generate this for you (@csrf in Laravel, {% csrf_token %} in Django). SameSite cookies help too. Never change data with GET requests — links and image tags can trigger those.

Login and sign-up forms that work with password managers

<form action="/signup" method="post">
  <label for="su-email">Email</label>
  <input id="su-email" name="email" type="email" autocomplete="username" required>
  <label for="su-pw">Password</label>
  <input id="su-pw" name="password" type="password" autocomplete="new-password" minlength="12" required aria-describedby="su-pw-hint">
  <p id="su-pw-hint">At least 12 characters.</p>
  <button>Create account</button>
</form>
  • autocomplete="username" on the identifier (even if it's an email), current-password on login, new-password on sign-up and change forms — password managers then offer to generate and save a password.
  • Don't block pasting, and don't split codes into one box per digit without handling paste.
  • For passkeys, autocomplete="username webauthn" lets the browser offer saved passkeys in the username field.

Bots: a honeypot field

<form action="/contact" method="post">
  <div class="visually-hidden" aria-hidden="true">
    <label for="website">Leave this field empty</label>
    <input id="website" name="website" tabindex="-1" autocomplete="off">
  </div>
  <label for="c-msg">Message</label>
  <textarea id="c-msg" name="message" required></textarea>
  <button>Send</button>
</form>

Simple bots fill every field; the server discards submissions where website isn't empty. It stops only unsophisticated bots — pair it with rate limiting.

File uploads

  • accept only filters the file picker. The server must check the file type (by content, not name), size, and store uploads outside the web root.
  • Show the maximum size in the label.

Double submission and feedback

Disable the submit button (with JavaScript) once the form is submitted, and show progress. After a successful POST, the server should redirect to a result page (the Post/Redirect/Get pattern), so reloading doesn't submit again.

Keeping forms usable

  • One column, labels above fields, and autocomplete everywhere.
  • Mark optional fields rather than required ones when most are required.
  • Keep values after a server-side error, and focus the error summary.
  • enterkeyhint="next" / "send" improves phone keyboards.

Related developer tools