Skip to content

Forms and Custom Validation ​

The browser's built-in receptionist says "Please fill out this field." Yours can say exactly what's wrong, and how to fix it.

In Form Validation, you let the browser check answers with attributes like required, minlength, and pattern. That's a great start, and it's still the foundation. But the built-in messages are generic, they look different in every browser, and you can't control where they appear.

That lesson ended with a promise: developers sometimes turn off the browser's checks with novalidate "when they write their own validation with JavaScript to show custom messages." This is that lesson.

The submit event ​

When a visitor submits a form, by clicking a submit button or pressing Enter in a field, the form fires a submit event. That's the one event to listen for:

js
const form = document.querySelector('#contact-form');

form.addEventListener('submit', (event) => {
	event.preventDefault();   // stop the page from reloading
	console.log('The form was submitted');
});

Listen on the form, not on the button's click. The submit event catches every way of submitting, including the Enter key, which a button's click listener would miss.

And event.preventDefault(), from Events, stops the browser's default behavior: sending the data and loading a new page. You'll handle the data yourself instead. (In a later lesson, Loading Data with fetch, you'll send it to a server without leaving the page.)

Reading the answers ​

You can read each field directly with .value, as you learned in Changing Text, Attributes, and Classes:

js
const name = form.querySelector('#contact-name').value.trim();

Or collect every answer at once with FormData, which reads each field's name attribute, exactly like the browser does when it sends a form:

js
const data = new FormData(form);
console.log(data.get('email'));

const answers = Object.fromEntries(data);
// { name: 'Maria', email: 'maria@example.com', message: '...' }

That's the payoff for the rule from Forms: The Basics: a field without a name is simply left out. Remember, too, that every value arrives as a string, so use Number() before doing any math.

Live feedback while typing ​

The input event fires on every keystroke, which makes it perfect for helpful live feedback, like a character counter:

js
const message = form.querySelector('#contact-message');
const counter = form.querySelector('#message-count');
const limit = 200;

message.addEventListener('input', () => {
	const remaining = limit - message.value.length;
	counter.textContent = `${remaining} characters left`;
});

Live feedback is great for things that help, like counters and password-strength hints. For error messages, it's usually kinder to wait until the visitor has finished with a field (or submits) before telling them it's wrong. Nobody likes being told off after typing one letter of their email address.

Two ways to customize validation ​

1. Keep the browser's checks, change the message ​

The browser's built-in validation has a JavaScript side, called the Constraint Validation API. Every field has a validity object describing what's wrong, and setCustomValidity lets you replace the message:

js
const email = form.querySelector('#contact-email');

email.addEventListener('input', () => {
	if (email.validity.typeMismatch) {
		email.setCustomValidity('That email looks incomplete. Did you forget the @ or the ending, like .com?');
	} else {
		email.setCustomValidity('');   // an empty message means "valid again"
	}
});

The catch is the last line: once you set a custom message, the field stays invalid until you clear it with an empty string. Forgetting to clear it is a classic bug, where a form refuses to submit even after the answer is fixed.

Useful validity checks include valueMissing (a required field is empty), typeMismatch (not a valid email or URL), tooShort, rangeOverflow, and patternMismatch. And field.checkValidity() returns true or false for a single field, or for the whole form.

One quirk worth knowing: tooShort only reports a problem after a person has actually edited the field, and it doesn't ignore spaces, so ' ' counts as two characters. For length rules, it's often simpler to check field.value.trim().length yourself, which is what the Try it below does.

2. Turn off the bubbles, show your own messages ​

For full control, add novalidate to the form. The browser stops showing its own bubbles, but all the validation attributes still work behind the scenes, so you can use them from JavaScript and show your messages right on the page:

html
<form id="contact-form" novalidate>
	<label for="contact-email">Email</label>
	<input type="email" id="contact-email" name="email" required aria-describedby="email-error">
	<p id="email-error" class="field-error"></p>
</form>
js
function showError(field, errorElement, message) {
	errorElement.textContent = message;
	field.setAttribute('aria-invalid', 'true');
}

function clearError(field, errorElement) {
	errorElement.textContent = '';
	field.removeAttribute('aria-invalid');
}

That HTML does two important accessibility jobs:

  • aria-describedby connects the field to its error message, so screen readers read the message when the field is focused. You met it in Form Validation.
  • aria-invalid="true" tells assistive technology the field currently has a problem.

And when the form is submitted with errors, move focus to the first field with a problem, so keyboard and screen reader users land right where they need to be:

js
// In your submit handler, after checking every field:
firstInvalidField.focus();

Good error messages follow the receptionist rule from the HTML track: say what's wrong and how to fix it. "Invalid input" helps nobody. "Your name needs at least 2 letters" does.

The security guard still matters ​

Everything in this lesson makes forms friendlier, but none of it makes them safe. Anyone can turn off JavaScript, edit your code in the developer tools, or send data straight to your server. As Form Validation put it: the browser is the receptionist, and the server is the security guard. Your server must check everything again, every time.

Try it ​

The preview blocks real form submissions, so the first few lines of the JavaScript are a small helper that sends the submit event by hand. You'd delete them on a real page.

HTML

CSS

JavaScript

Preview

Console

Nothing logged yet. Anything you pass to console.log() shows up here.

Try it yourself ​

  1. Press "Send message" with everything empty. Where does focus go? Then fill in a one-letter name and an email without an @, and try again.
  2. Fill everything in correctly and submit. Look at the object in the console: every key comes from a field's name.
  3. Add a "Phone" field with type='tel', name='phone', and a pattern of your choice, plus a message for validity.patternMismatch in errorFor.

Check your understanding ​

Which event should you listen for to handle a form being sent?
You called field.setCustomValidity('Too short') and the visitor fixed their answer, but the form still won't submit. Why?
With JavaScript validation on the page, does the server still need to check the data?

Up next ​

That completes the DOM chapter. You can find, change, build, and listen to anything on the page. Next, we go deeper into how JavaScript works, starting with a word that confuses everyone at first. That's this and Classes.

← Events
this and Classes →

Answer the quiz above correctly to continue.