Forms

12 min

Forms

Forms are where craft meets function most visibly. A form with missing labels, broken autocomplete, and validation that punishes users for typing is not a design problem—it's a respect problem. Every form detail in this lesson takes minutes to implement and hours to ignore when users start complaining.

Labels are not optional

Every form input needs a visible label—a piece of text that says what to put in the field. Labels are accessibility-critical (screen readers read them aloud when focus enters), keyboard-friendly (clicking a label focuses its input), and trust-building (forms with naked placeholders feel rushed).

Three things to do for every input:

<label for="email">Email address</label>
<input
  id="email"
  type="email"
  name="email"
  autocomplete="email"
  inputmode="email"
/>

What each attribute does:

  • <label for="..."> — binds the label to the input. Click the label, focus moves to the input.
  • type="email" — changes the on-screen keyboard on mobile, enables browser validation, and signals semantic intent.
  • name="email" — matters for form submission and password manager matching.
  • autocomplete="email" — tells the browser this is the user's email so it can offer autofill. Use the spec values: email, name, current-password, new-password, street-address, tel, cc-number, one-time-code.
  • inputmode="email" — controls the on-screen keyboard layout without changing validation. Useful when type="text" is needed but you want the email keyboard.

If the design genuinely can't accommodate a visible label (tiny inline forms, search boxes), use a visually-hidden label rather than a placeholder. The label remains in the DOM, announced by screen readers, but takes no visual space.

Autocomplete matters more than you think

autocomplete is the single most under-used form attribute. It's how the browser knows to offer the user's saved email, their shipping address, their credit card number. Without it, users type everything manually—or worse, the browser guesses wrong and autofills the wrong field.

Use the exact spec values. autocomplete="username" is not the same as autocomplete="email". autocomplete="new-password" triggers the browser's password generator; autocomplete="current-password" triggers the saved credential. Getting these wrong means the browser saves the email as the username for the wrong site, or offers to generate a password on a login form.

Don't use autocomplete="off" on auth fields. It doesn't improve security—it just breaks password managers.

inputmode for the right keyboard

inputmode controls which on-screen keyboard appears on mobile:

  • inputmode="numeric" — number pad. For amounts, quantities, verification codes.
  • inputmode="decimal" — number pad with a decimal point. For currency input.
  • inputmode="tel" — phone-style keypad with +, -, and #.
  • inputmode="email" — includes @ and . prominently.
  • inputmode="url" — includes / and .com.
  • inputmode="search" — includes a search/return key.

The difference between type="number" and inputmode="numeric" is important: type="number" adds browser validation and spinner arrows; inputmode="numeric" just shows the number keyboard. For a verification code field, you want the keyboard but not the spinner.

Enter-to-submit

A single-line form should submit on Enter. This is the default browser behaviour when a <button type="submit"> exists inside a <form>—but custom React forms often break it by handling submission entirely through onClick. Wrap your inputs in a <form> with an onSubmit handler and a submit button:

<form onSubmit={handleSubmit}>
  <input type="text" />
  <button type="submit">Search</button>
</form>

For multi-line text areas, Enter should insert a newline. Use Cmd/Ctrl+Enter for submission—the convention established by Slack, Linear, and GitHub.

Inline errors

Show errors next to the field they apply to, not in a summary block at the top. The user sees which field is wrong, why, and how to fix it—without scrolling.

When to validate:

  • Not on every keystroke for fields the user is still filling in. "Email is invalid" flashing while they're typing is condescending.
  • On blur is the sweet spot. The user moves to the next field; validate the previous one.
  • On submit for required-field checks and cross-field validation.

On submit failure, focus the first error. The user lands on the problem; their next keystroke fixes it. Without this, they scroll, find the error, click into the field—three actions instead of one.

Be specific: "Email is required. Try the email you used at signup." Not "Invalid input."

Don't disable the submit button until every field is filled. The user might want to see all errors at once. Keep submit enabled; on click, show every error inline. Disable only during the in-flight request itself.

Mobile font size: the 16px rule

On iOS, Safari automatically zooms the page when a user focuses an input whose font size is less than 16px. The reasoning: the browser thinks the text would be unreadable. The result is a busted layout—the page scales to 1.4x, content gets cut off, and the user has to manually zoom back out.

The fix:

input, select, textarea {
  font-size: 16px;
}

If the design calls for smaller-looking inputs, override only on desktop:

@media (min-width: 1024px) {
  .input { font-size: 14px; }
}

In Tailwind, text-sm is 14px—the most common offender. Explicitly set inputs to text-base (16px) or larger.

Form UX Lab

On iOS, email and phone fields show the right keyboard.

Toggle each control to see how a single attribute changes the form.

Never block paste

Don't intercept onPaste to "validate" passwords, credit card numbers, or verification codes. Paste is how users get values from password managers. Blocking it means they retype a 32-character password by hand.

If the field can't accept arbitrary characters, validate after the paste—not by preventing it:

<input onChange={(e) => setValue(e.target.value.replace(/\s/g, "").trim())} />

Common mistakes

  • Placeholder as the only label. Once the user types, the placeholder disappears—they can't remember which field this was.
  • Wrong autocomplete values. The browser saves the email as the username for the wrong site.
  • onPaste={(e) => e.preventDefault()} on a confirm-password field. Breaks password managers.
  • Tailwind text-sm on inputs without a mobile override. iOS zooms.
  • Disabled submit until the form is "complete." The user has no way to discover they're missing a field.
  • Spellcheck and autocapitalize on email/code fields. Add spellcheck="false" and autocapitalize="off".