HTML form validation

The browser validates a form from attributes alone. required, type, pattern, min, max and step cover most rules, and a few lines of script handle the wording.

HTML form validation runs from attributes on the input elements, with no script involved. The browser blocks submission and shows a message on the first failing field.

<form>
  <label for="email">Work email</label>
  <input id="email" name="email" type="email" required>
  <button>Send</button>
</form>

Two attributes, type="email" and required, and the form already refuses an empty box and a string with no at sign.

A form submitted with an empty required field, showing the browser's own message bubble.
A form submitted with an empty required field, showing the browser's own message bubble.

Attribute reference

Attribute Applies to Enforces
required Most inputs, select, textarea A value is present
type="email" input Basic address shape
type="url" input A parseable absolute URL
type="number" input Numeric value only
min / max number, date, range Value within bounds
step number, date, time Value on the interval
minlength / maxlength text, textarea Character count
pattern text, tel, search, url A regular expression match

maxlength behaves differently from the rest. It prevents typing past the limit rather than reporting an error, so it silently truncates a paste.

Patterns

pattern takes a regular expression that must match the whole value. No anchors are needed, and they are implied.

<input name="code" pattern="[A-Z]{2}-\d{4}" title="Two letters, a hyphen, four digits">

The title attribute supplies the hint the browser appends to its message. It is one of the few places the title attribute has a defined job, though a visible hint under the field is better for touch users.

Keep patterns loose. A strict email regular expression rejects valid addresses, and type="email" already covers the useful case.

Styling validity without the red wall

:invalid matches from load, so an empty required field is red before the reader has done anything.

/* the trap */
input:invalid { border-color: #ef4444; }

/* what to use instead */
input:user-invalid { border-color: #ef4444; }
input:user-valid   { border-color: #22c55e; }

:user-invalid only matches after the reader has interacted with the field or tried to submit. That is the behaviour people expect from validation styling.

A form where only the field the reader has left empty is marked, the rest neutral.
A form where only the field the reader has left empty is marked, the rest neutral.

For required fields, mark them visibly in the label as well. Colour alone is not a usable signal.

Custom messages

The default wording is set by the browser and localised to its language. To replace it, use setCustomValidity.

<input id="code" name="code" pattern="[A-Z]{2}-\d{4}" required>
<script>
  const el = document.getElementById('code');
  el.addEventListener('invalid', () => {
    el.setCustomValidity(
      el.validity.valueMissing
        ? 'Enter the reference code from the invoice.'
        : 'Format is two letters, a hyphen, then four digits.'
    );
  });
  el.addEventListener('input', () => el.setCustomValidity(''));
</script>

The second listener is not optional. A non-empty custom message counts as a validity failure, so without clearing it the field can never become valid again.

el.validity exposes which rule failed: valueMissing, typeMismatch, patternMismatch, rangeUnderflow, tooShort and several more.

Turning the native messages off

Sometimes the design calls for messages in the page rather than browser bubbles.

<form novalidate>

novalidate suppresses the bubbles and the submit block, but the validity state is still computed. You can read form.checkValidity() and render your own errors from el.validity.

That is the supported way to take over. Removing the attributes instead loses the state as well.

Accessibility

  • Every field needs a label. A placeholder is not a label; it disappears on typing. Use <label for>, or aria-label where no visible text exists.
  • Link the error to the field with aria-describedby pointing at the error element's id.
  • Mark the field with aria-invalid="true" when it fails, so a screen reader announces the state.
  • Move focus to the first failing field on a failed submit.
<label for="qty">Quantity</label>
<input id="qty" type="number" min="1" max="99" aria-describedby="qty-err" required>
<p id="qty-err" class="err">Between 1 and 99.</p>

Rules the attributes cannot express

Four common requirements have no attribute, and each has a standard workaround.

  1. Two fields must match, for example a confirmation. Compare them in an input handler and call setCustomValidity on the second.
  2. One field is required only if another is filled. Toggle the required property in script when the first changes.
  3. A date must be after another date. Set the second field's min from the first field's value.
  4. At least one checkbox in a group. Set required on all of them, then clear it in script as soon as one is checked.
<script>
  const from = document.getElementById('from');
  const to   = document.getElementById('to');
  from.addEventListener('input', () => { to.min = from.value; });
</script>

Driving an attribute from script keeps the browser doing the enforcement, which means the messages, the focus behaviour and the :user-invalid styling all keep working.

Input types worth using

Type Keyboard on mobile Validates
email At sign visible Address shape
tel Numeric keypad Nothing, pair with pattern
url Slash and dot visible Absolute URL
number Numeric keypad Numeric, plus min, max, step
date Date picker A real date
search Search key Nothing

tel validates nothing on purpose, because phone number formats vary too widely. Use it for the keypad and add a loose pattern if a format is genuinely required.

Avoid type="number" for things that are not quantities. Order references and postal codes lose leading zeros and gain spinner arrows.

The form element's own attributes

<form action="/submit" method="post" novalidate autocomplete="on">

autocomplete deserves more attention than it gets. Naming the fields correctly lets the browser fill them, which removes more errors than any validation rule.

<input name="email" type="email" autocomplete="email">
<input name="org"   type="text"  autocomplete="organization">

The token list is defined, so autocomplete="email" is understood while autocomplete="work-email" is not and falls back to a guess.

Validation is not security

Everything above runs in the reader's browser and can be edited out in seconds. It exists to help someone fill the form correctly, not to guarantee what arrives.

Any form whose values reach a system must repeat every check on the receiving side.

Testing the form as a reader sees it

The form opened from a share link on a phone, with the number keypad shown.
The form opened from a share link on a phone, with the number keypad shown.

Input types change the on-screen keyboard on mobile. type="email" brings the at sign, type="number" brings the keypad. That only shows on a real device.

Open the file in the HTML file opener to confirm the markup renders outside your project. Then paste the HTML into a document and turn it into a link, and open that link on a phone to check the keyboards and the error wording.

Related: turning a form into a link, semantic HTML for the structure underneath, and editable HTML table when the input is tabular rather than a form.

Questions people ask

Does HTML form validation need JavaScript?

Not for the rules themselves. required, type, pattern, minlength, min, max and step are enforced by the browser on submit. JavaScript is only needed to reword the error messages or to validate something the attributes cannot express.

Why does every field turn red before the user types anything?

The :invalid selector matches from page load, because an empty required field is already invalid. Use :user-invalid instead, which only matches after the user has interacted with the field or attempted to submit.

Can I change the wording of the browser error message?

Yes, with setCustomValidity on the input. Set your message in an invalid handler and clear it to an empty string on input, otherwise the field stays permanently invalid because a non-empty custom message is itself a validity failure.

Is built-in validation enough on its own?

For the reader experience, largely yes. For trust, no. Anything enforced only in the browser can be bypassed, so a form that reaches a server or a database must repeat every check there.

Keep reading