🌐

HTML Form Validation: Attributes and the Constraint API

HTML/CSS4 min readUpdated October 2026

Validate forms with plain HTML attributes, style valid and invalid states with CSS, and use the Constraint Validation API for custom messages.

Before you reach for a validation library, know that browsers already do a lot of the work. With a few HTML attributes you get error bubbles, blocked submissions, and the right keyboard on phones, with no JavaScript at all.

One rule first: client-side validation is for convenience, not security. Anyone can bypass it. Your server must check everything again. The browser checks are there to help honest users fix mistakes quickly.

Start with the right input types

The type attribute tells the browser what kind of data to expect. It validates the format and shows the best keyboard on mobile.

<form>
  <label>Email <input type="email" name="email" required></label><br><br>
  <label>Website <input type="url" name="site"></label><br><br>
  <label>Age <input type="number" name="age" min="13" max="120"></label><br><br>
  <button>Send</button>
</form>

Try submitting with a bad email. The browser blocks the form and points at the problem. email, url, number, date, and tel are all worth using.

The core validation attributes

  • required means the field can't be empty.
  • minlength / maxlength limit the length of text.
  • min / max limit numbers and dates.
  • step controls which numbers are allowed (for example step="5").
  • pattern checks the value against a regular expression.
<form>
  <label>Username
    <input name="user" required minlength="3" maxlength="16"
           pattern="[a-z0-9_]+" title="Lowercase letters, numbers, and underscores only">
  </label>
  <br><br>
  <label>Password
    <input type="password" name="pw" required minlength="8">
  </label>
  <br><br>
  <button>Create account</button>
</form>

Always add a title when you use pattern. Browsers show it as part of the error message, which tells people what the rule actually is. A pattern is automatically anchored to the whole value, so you don't need ^ and $.

Always use labels

Every input needs a visible label. It improves accessibility, and clicking the label focuses the field, which helps on touch screens. Wrapping the input in the label, as above, is the simplest way. You can also use for and id:

<label for="city">City</label>
<input id="city" name="city" type="text" autocomplete="address-level2">

Add the correct autocomplete value, too. Browsers can then fill in names, addresses, and cards, and completion rates go up.

Style valid and invalid states

CSS has pseudo-classes for validation. There's a catch, though: if you style :invalid directly, empty required fields turn red before the user has touched anything. Use :user-invalid, which only applies after interaction.

<style>
  input { padding: 8px; border: 2px solid #ccc; border-radius: 6px; }
  input:user-invalid { border-color: #dc2626; background: #fef2f2; }
  input:user-valid   { border-color: #16a34a; }
</style>

<form>
  <label>Email <input type="email" required></label>
  <button>Go</button>
</form>

Type something wrong, then click away from the field. The border changes only after you've had a chance to fill it in.

Custom messages with the Constraint Validation API

The default messages are plain. For better wording, use JavaScript. Every field has a validity object that tells you exactly what's wrong, and setCustomValidity() lets you replace the message.

<form id="signup">
  <label>Age <input id="age" type="number" min="18" max="99" required></label>
  <p id="msg" style="color:#b91c1c"></p>
  <button>Join</button>
</form>

<script>
  const age = document.querySelector("#age");

  age.addEventListener("input", () => {
    age.setCustomValidity("");           // clear old message first
    if (age.validity.rangeUnderflow) {
      age.setCustomValidity("You need to be at least 18 to join.");
    } else if (age.validity.valueMissing) {
      age.setCustomValidity("Please tell us your age.");
    }
  });

  document.querySelector("#signup").addEventListener("submit", (event) => {
    event.preventDefault();
    document.querySelector("#msg").textContent = "Submitted! (demo only)";
  });
</script>

You must reset the message to an empty string when the value becomes valid again. Otherwise the field stays stuck as invalid.

The validity object has flags such as valueMissing, typeMismatch, patternMismatch, tooShort, rangeUnderflow, and rangeOverflow. Check the one you care about and respond with a specific message.

Showing messages inline instead of in bubbles

Browser bubbles can't be styled. If you want errors in your own design, turn off the default behavior with novalidate and validate in your own handler:

<form id="f" novalidate>
  <label>Email <input id="email" type="email" required></label>
  <span id="err" style="color:#b91c1c"></span>
  <button>Subscribe</button>
</form>

<script>
  const form = document.querySelector("#f");
  const email = document.querySelector("#email");
  const err = document.querySelector("#err");

  form.addEventListener("submit", (event) => {
    event.preventDefault();
    if (email.checkValidity()) {
      err.textContent = "";
      alert("Thanks!");
    } else {
      err.textContent = email.validationMessage;
    }
  });
</script>

checkValidity() returns true or false, and validationMessage gives you the browser's text, which you can then show wherever you like.

Make errors easy to understand

Good error messages are specific and polite. Compare "Invalid input" with "Use 8 or more characters." Say what's wrong, then say how to fix it. Don't rely on color alone, since some users can't tell red from green. Add text or an icon, and use aria-describedby to connect error text to its field for screen readers.

Common mistakes

  • Trusting the browser. Always validate again on the server.
  • Using pattern for emails. Use type="email". Email rules are too complicated for a short regex.
  • Blocking users with strict rules. Names, phone numbers, and addresses vary around the world. Don't reject valid people.
  • Forgetting to clear custom messages. An old setCustomValidity message will block submission.

Key takeaways

  • Use the right type and attributes first. They cover most needs.
  • Style with :user-invalid, not :invalid.
  • setCustomValidity and validity give you friendly, custom messages.
  • Client-side checks are a convenience. The server is the real gatekeeper.

Try it yourself

Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.

Open the HTML/CSS editor

Related Articles