completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Form validation

Validate browser form fields with required, type, pattern, validity, setCustomValidity, custom messages, and accessible error UI while still trusting the server for final checks.

By the end, you can
  • 01
    Use the built-in HTML rulesAdd rules to a field with required, the input type, pattern, min, max, step, minlength, and maxlength.
  • 02
    Read and change the checklistRead the flags in validity, tell checkValidity() and reportValidity() apart, and add or remove your own error with setCustomValidity().
  • 03
    Write errors everyone can useShow your own messages, a summary, and a focus move, linked to each field with aria-invalid and aria-describedby.

What form validation does

Form validation means checking what a person typed into a form before you accept it. Each check is a rule. For example:

  • A signup form needs an email address.
  • A booking form needs a date after today.
  • A coupon box takes only a code shaped like SAVE20.

The browser can run many of these checks for you. Its tools for this are called the Constraint Validation API. A constraint is one rule for one field. You write the rules as HTML attributes. Then JavaScript can:

  • read input.validity to see which rules fail,
  • call checkValidity() to test a field or a whole form,
  • call setCustomValidity() to add a rule of your own.
A tiny password rulePop out in the code editor (opens in a new tab)JavaScript
function passwordMessage(password, confirm) {  return password === confirm ? "" : "Passwords must match";} console.log(passwordMessage("otter", "owl"));console.log(passwordMessage("otter", "otter"));

The function compares two passwords. Line 2 gives back an empty string, "", when they match. When they don't match, it gives back the message Passwords must match. Here the empty string means "no error".

So line 5 prints Passwords must match, and line 6 prints an empty line. Later in this lesson, you'll pass this same result to confirm.setCustomValidity().

Real-life analogyValidation is a bouncer with a checklist

A bouncer is the person at a club door who checks each guest. The bouncer goes down a checklist. Does the guest have a ticket? Is it tonight's ticket? Is the guest old enough? Each rule gets its own tick or cross.

The browser does the same for a form field. It keeps one flag for each rule. So you can see exactly which rule failed.

In real life: The guest must have a ticket
In JavaScript: required: the field can't be empty
In real life: The ticket must look like tonight's ticket
In JavaScript: pattern and the input type: the value must have the right shape
In real life: The guest must be old enough
In JavaScript: min, max, and step: a number or date must be in the allowed range
In real life: A tick or a cross for each rule
In JavaScript: validity: one true-or-false flag for each rule, such as valueMissing
Real-life analogyThe browser check is a receptionist, not a security guard

A receptionist helps visitors fill in their forms correctly. But the receptionist doesn't keep the building safe. Security does that.

The browser check is the receptionist. It helps people fix small mistakes while the form is still on the screen. The server is the computer that receives and saves the form data. It is the security guard. The browser check can't prove the data is safe, because anyone can send data to your server without using your page.

In real life: The receptionist points out a missing signature
In JavaScript: The browser points out simple mistakes right away
In real life: The manager adds a rule just for today
In JavaScript: setCustomValidity('Passwords must match') adds your own rule
In real life: Security checks the ID again later
In JavaScript: The server checks the important rules again

required, pattern, and input types

STEP THROUGH

Start with HTML. The browser already knows many common rules. These rules also help other tools understand the field, such as phone keyboards, password managers, and screen readers.

Each field has a validity object. It holds one flag for each rule. A flag is a true-or-false value, and it is true when its rule fails. Here are the main rules and their flags:

  • required means the field can't be empty. An empty required field turns on validity.valueMissing.
  • type="email" and type="url" want an email address or a web address. A value in the wrong shape turns on typeMismatch.
  • type="number" and the date types accept only a number or a date.
  • pattern gives a shape that the whole value must match. It is skipped when the field is empty. You'll learn the pattern language in the Regular expressions lesson. For now, read [A-Z]{3} as "three capital letters".
  • min, max, and step set which numbers or dates are allowed.
  • minlength and maxlength set how long the text may be. Browsers turn on tooShort only after a person has typed the value.
Three small rule failuresPop out in the code editor (opens in a new tab)JavaScript
const form = document.createElement("form");const field = document.createElement("input");field.required = true;field.type = "email";field.minLength = 5;form.append(field);document.body.append(form); console.log(field.validity.valueMissing);field.value = "asha@";console.log(field.validity.typeMismatch);field.value = "ash";field.dispatchEvent(new Event("input", { bubbles: true }));console.log(field.validity.tooShort);

Lines 1 to 7 make a form with one email field. The field is required, must be an email address, and needs at least 5 characters (minLength). Then:

  • Line 9 prints true for valueMissing, because the field is still empty.
  • Line 10 puts asha@ in the field. That is not a full email address, so line 11 prints true for typeMismatch.
  • Line 12 puts ash in the field, which has only 3 characters. Even so, line 14 prints false for tooShort.

Why false? A value set by code doesn't count as typed, even with the made-up input event on line 13. To see tooShort turn on, type into the dashboard in the next section.

The pattern must match the whole value, from the first character to the last. So [A-Z]{3} accepts ABC but not ABCx. Current browsers read the pattern in a strict mode, the regular expression v flag. In that mode, some punctuation inside square brackets needs a backslash before it.

Some fields skip validation completely. For those fields, input.willValidate is false, and the browser never checks their rules. This happens for:

  • disabled fields,
  • read-only text fields,
  • fields with type="hidden".
<label>  Age  <input name="age" type="number" min="13" max="120" step="1" required></label>

This field needs a whole number from 13 to 120. Line 3 holds all the rules. Each wrong value turns on a different flag:

  • An empty field turns on valueMissing, because of required.
  • 12 turns on rangeUnderflow, which means "below min".
  • 121 turns on rangeOverflow, which means "above max".
  • 13.5 turns on stepMismatch, because step="1" allows only whole numbers.

The browser checks these rules before your submit code runs.

A PIN and a ticket countPop out in the code editor (opens in a new tab)JavaScript
const pin = document.createElement("input");pin.pattern = "[0-9]{6}";pin.value = "1234567";console.log(pin.validity.patternMismatch);pin.value = "123456";console.log(pin.validity.patternMismatch); const tickets = document.createElement("input");tickets.type = "number";tickets.min = "2";tickets.max = "10";tickets.step = "2";tickets.value = "3";console.log(JSON.stringify({ underflow: tickets.validity.rangeUnderflow, step: tickets.validity.stepMismatch }));

Line 2 says the PIN must be exactly six digits. Line 4 prints true for patternMismatch, because 1234567 has seven digits. The pattern must match the whole value, so six of the seven digits is not enough. Line 6 prints false, because 123456 matches.

Lines 8 to 13 make a ticket field that allows 2 to 10, in steps of 2. So the allowed values are 2, 4, 6, 8, and 10. Line 14 prints {"underflow":false,"step":true}. The value 3 is inside the range, so rangeUnderflow is false. But 3 is not one of the steps, so stepMismatch is true.

Step through a field's checklist
Step 0 of 5Ready
Your turn: follow the blue line

Pick a field, guess the flags, then step through the checklist.

Running in
  1. script
Next: line 1
const validity = check(field);console.log(validity.valueMissing);console.log(validity.patternMismatch);console.log(validity.valid);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick a field to check

Each choice starts the steps again. Ask the same question each time: which flags turn on?

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Compare the first two choices. In both, the field is empty and has a pattern. The pattern flag stays false in both, because the pattern waits for a value. Only required decides if an empty field is allowed.

Two empty fields that both have a pattern. Left: required is false, so the pattern is skipped and valid is true. Right: required is true, so valueMissing is true and valid is false.required: false""fieldpattern is skippedvalid: truerequired: true""fieldvalueMissing: truevalid: false
Both fields are empty and both have a pattern. The pattern waits for a value, so only required decides if an empty field is allowed.

So if an empty field should be an error, add required. If the field is optional, people can leave it blank without seeing a format error.

Read the validity checklist

INTERACTIVE

Every field that takes part in validation has a validity property. Its value is a ValidityState object: a checklist with one flag for each kind of failure. It also has one last flag, valid. valid is true only when every other flag is false.

The email field holds "asha@". Its validity checklist says valueMissing: false, typeMismatch: true, tooShort: false. One rule fails, so valid is false."asha@"emailvalueMissing: falsetypeMismatch: truetooShort: falsethis rule failsvalid: false
The email field from the first example, after it gets "asha@". Only the email rule fails, and that one failed rule makes valid false.

The dashboard below shows the real flags from your browser. It reads them when the page loads and each time you type.

It also shows validationMessage: the error text the browser writes for that field. The browser chooses those words, so they differ between browsers and languages. For the text in your own app, write your own messages.

Validity dashboard
The rules for each fieldPop out in the code editor (opens in a new tab)HTML
<form>  <input name="name" required>  <input name="email" type="email" required>  <input name="site" type="url">  <input name="tickets" type="number" min="1" max="10" step="2">  <input name="code" minlength="3" maxlength="8" pattern="[A-Z]{3}[0-9]{2}">  <input name="day" type="date" min="2026-01-01"></form>
Live form0 valid
Try it yourself
Type in the fields. The checklist updates as you type.

The cards read each field's real validity object and the browser's validationMessage. They update when the page loads and each time you edit a field. The message words can differ between browsers and languages.

A real form next to its real validity flags.

Try these:

  • Leave Name empty: valueMissing turns on.
  • Type abc in Email: typeMismatch turns on.
  • Type 4 in Tickets: stepMismatch turns on, because the allowed values are 1, 3, 5, 7, and 9.
  • Type ab in Code: tooShort turns on, because you typed it yourself. patternMismatch turns on too.
Which validity flag?
  • An empty required field
  • abc in type=email
  • 7 in min=0 step=5
  • A password message set with setCustomValidity
Try it yourself
0 of 4 correct

Sort each case by the flag it turns on.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Length and pattern flags
  • 12 typed into a text field with minlength=3
  • abcdef in a field with maxlength=4
  • ab12 in a field with pattern="[A-Z]{2}[0-9]{2}"
Try it yourself
0 of 3 correct

Now sort these text problems by the flag they turn on.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

validity and setCustomValidity

INTERACTIVE

HTML attributes check one field at a time. But many real rules compare two fields, or use your app's own data. For example:

  • The two passwords must match.
  • The end date must come after the start date.
  • A username can't be one that your app keeps for itself.

For rules like these, use setCustomValidity(message). It works like this:

  • Any message that isn't empty marks that one field as invalid. It turns on validity.customError.
  • setCustomValidity(""), with an empty string, removes the error.
  • The error stays until you remove it. The empty string is the only way to turn it off.
confirm.setCustomValidity with the message "Passwords must match" makes customError true, so the field is invalid. setCustomValidity with the empty string makes customError false, so the field is valid.confirm.setCustomValidity(…)"Passwords must match"customError: truea messageinvalid""customError: falseempty stringvalid
Any message turns customError on. Only the empty string "" turns it off again.
setCustomValidity lab
Check that the passwords matchPop out in the code editor (opens in a new tab)JavaScript
confirm.addEventListener("input", () => {  const match = password.value === confirm.value;  confirm.setCustomValidity(match ? "" : "Passwords must match");}); form.checkValidity();  // boolean, fires invalidform.reportValidity(); // boolean, asks browser to show UI
Try itinvalid events: 0
Try it yourself

Make the two passwords match, then try the buttons.

Any message that isn't empty keeps the confirm field invalid, until the code calls setCustomValidity("").

Three methods have similar names. Read each row as: what you call, what you get back, and what it does.

Three validation methods with similar names
What you callWhat you get backWhat it does
input.checkValidity() or form.checkValidity()true or false.It checks the rules and fires an invalid event on each field that fails. It shows nothing to the person.
input.reportValidity() or form.reportValidity()true or false.It checks the rules and asks the browser to show the problem to the person.
input.setCustomValidity(message)undefined.It sets or removes your own error on one field. Any message that isn't empty makes the field invalid.
Check quietly, then reportPop out in the code editor (opens in a new tab)JavaScript
const form = document.createElement("form");const email = document.createElement("input");email.type = "email";email.required = true;form.append(email);document.body.append(form);let invalidEvents = 0;form.addEventListener("invalid", () => invalidEvents++, true); console.log(form.checkValidity());console.log(form.reportValidity());console.log(invalidEvents);

The email field is required and empty, so both checks fail. Line 10 prints false for checkValidity(), and line 11 prints false for reportValidity().

The invalid event is the browser's signal that a field failed a check. Line 8 counts these events, and line 12 prints 2: one event for each check. checkValidity() only tests. reportValidity() tests and also shows the browser's error bubble, a small pop-up next to the field.

Set your own message when a field failsPop out in the code editor (opens in a new tab)JavaScript
const form = document.createElement("form");const email = document.createElement("input");email.required = true;form.append(email);document.body.append(form);form.addEventListener("invalid", (event) => {  event.target.setCustomValidity("Email is required");}, true);form.checkValidity();console.log(email.validationMessage);

Line 6 listens for invalid on the form. Line 9 checks the form, and the empty field fails. So the listener runs, and line 7 sets your own message on that field. Line 10 prints Email is required instead of the browser's words.

The invalid event does not bubble up from the field to the form. That is why line 8 passes true. With true, the listener runs in the capture phase, while the event travels down to the field. So a listener on the form still hears it.

To pick a message for a field, follow these steps:

  1. Read validity.valid.
  2. If it is false, find the first flag that is true.
  3. Show your own message for that flag.

Don't use validationMessage as your app's text. The browser chooses its words and its language.

Custom error messages

INTERACTIVE

The browser's error bubbles are fine for a quick test. Real apps often want more:

  • the same wording on every page,
  • a list of all the errors at the top of the form,
  • messages in the person's own language,
  • each message placed right next to its field.

To do this, add novalidate to the form. It turns off the browser's own bubbles. Then call checkValidity() yourself and show your own messages.

A good message says how to fix the field. Use a real email address, like ada@example.com helps more than Invalid input. Read the same flags as before, and turn each one into a clear instruction.

Your own error messages with novalidate
Show your own errors on submitPop out in the code editor (opens in a new tab)HTML + JavaScript
<form novalidate>  <input id="email" type="email" required aria-describedby="email-error">  <p id="email-error"></p>  <button>Join</button></form> form.addEventListener("submit", (event) => {  event.preventDefault();  if (!form.checkValidity()) showFriendlyErrors();});
Your messages0 errors

Try it yourself

Submit the form to see your own messages instead of the browser's bubbles.

The form has novalidate, so your submit code runs. It shows clear messages based on the same flags.
novalidate lets your submit code runPop out in the code editor (opens in a new tab)JavaScript
const form = document.createElement("form");form.noValidate = true;const email = document.createElement("input");email.required = true;form.append(email);document.body.append(form);form.addEventListener("submit", (event) => {  event.preventDefault();  console.log("submit -> " + form.checkValidity());});form.requestSubmit();

Line 2 turns on novalidate. The email field is required and empty. Line 11 calls requestSubmit(), which submits the form the same way a submit button does.

Normally the browser would stop that submit. With novalidate, it reaches your submit listener. But the rules are still there: line 9 prints submit -> false, because checkValidity() still sees the empty field. Use novalidate only when your own code shows an equally clear error.

When should messages appear? Many teams wait for the first submit, so people don't see red errors while they are still typing. After that, they update the messages on input (each edit) or on blur (when the person leaves the field).

CSS has two selectors for this. :invalid matches a field as soon as it breaks a rule, even before the person touches it. The newer :user-invalid matches only after the person has changed the field, in browsers that support it.

CSS shows the error, but it doesn't replace the message

Use :invalid when the state should show right away. Use :user-invalid when a field should look wrong only after the person changes it. Older browsers just skip it. Neither selector writes the message, moves focus, or replaces the server check.

Accessible errors

INTERACTIVE

A red border alone doesn't help everyone. Some people use only a keyboard. Some use a screen reader, a program that reads the page aloud. Some zoom the page in a lot. They all need the error as text, and the text must be linked to its field in the HTML.

  • Set aria-invalid="true" on each field that has an error. This tells screen readers the field is invalid.
  • Set aria-describedby on the field to the id of its error text. A screen reader then reads the error together with the field.
  • Put a summary of all the errors near the top, with a link to each field.
  • After a failed submit, move focus to the first invalid field, or to the summary. Focus is the place where typing goes.
  • Make the summary a live region with aria-live="polite". A screen reader then reads new errors aloud when it is free.
Link the error to its field
The attributes that link an errorPop out in the code editor (opens in a new tab)HTML
<div role="alert" aria-live="polite" id="error-summary"></div><input id="email" required aria-invalid="true" aria-describedby="email-error"><p id="email-error">Enter your email address.</p>
On the pageerror on

Enter your email address.

Try it yourself

When the error shows, aria-invalid tells screen readers the field is invalid. aria-describedby links the field to its message. The summary is a live region, so new errors are read aloud.

Use these attributes together with your own messages and a focus move.

The form events lesson showed how to move focus and how to handle submit. Validation uses both. Catch the failed submit, show a summary, and move focus. Then keep each message linked to its field while the person fixes it.

Where you will use this

A signup form can use every tool from this lesson:

  • The email field has type="email" required.
  • The password field has minlength="8".
  • The confirm field uses setCustomValidity() to check that the passwords match.
  • On submit, your code shows its own summary of the errors.

Other forms use them too. At checkout, min, max, and step check a quantity. In settings, setCustomValidity() can block a username that your app keeps for itself.

A good habit

Use the built-in HTML rules first. Add setCustomValidity() only for rules that HTML can't express. Always check again on the server. The browser check helps people; the server check protects the data.

Once a form passes, the next lesson, FormData, shows how to collect its values with new FormData(form) and get them ready to send.

The server checks the important rule againPop out in the code editor (opens in a new tab)JavaScript
function canCreateAccount(email) {  const value = String(email ?? "").trim();  return value !== "" && value.includes("@");} console.log(canCreateAccount("ada@example.com"));console.log(canCreateAccount(""));

The server gets plain data, with no browser rules attached. Line 2 turns whatever arrived into a string and removes the spaces at both ends. The ?? part turns null or undefined into an empty string first. Line 3 rejects an empty value and a value with no @.

So line 6 prints true, and line 7 prints false. A real server would use a full email check and its own rules before it creates the account.

Your page checks the form and sends the data to the server. A script can also send data straight to the server and skip the page. The server checks again and saves only valid data.your pagebrowser checkhelps peoplesends datascriptskips the pageserverchecks againsaves only if valid
Your page checks first, to help people fix mistakes. A script can skip your page, so the server checks again, like canCreateAccount(), before it saves anything.

Common misconceptions

  • "The browser accepted it, so it is safe." Not true. The server must check every rule that matters.
  • "pattern makes a field required." No. An empty optional field skips pattern. Add required for that.
  • "A custom error removes itself." No. You must call setCustomValidity("") to remove it.
  • "validationMessage is good text for my app." It is the browser's text, in the browser's language. When the words matter, map each flag to your own message.
  • "A red border is enough." No. People need the error as text, linked to its field, plus a summary and a focus move.

Each row below is a tempting shortcut, and a better habit to use instead.

Tempting shortcuts and better habits
The shortcutDo this instead
Check every rule with JavaScript if statements.Write the rules as HTML attributes, so the browser, phone keyboards, and screen readers know about them.
Show every error as soon as the page loads.Wait for the first submit or for the person to change the field. Then show clear messages.
Trust the browser check.Check again on the server before you save data, take a payment, or log someone in.
Show one general error for everything.Turn the flag that failed into a clear next step.

Practice exercises

4 EXERCISES
Exercise 1 · Warm-upName the flag for an empty required field

A required text field is empty. Type the name of the flag that becomes true.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log("valueMissing");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeRemove the password error

    Read the code. It sets an error on the confirm field when the passwords don't match. What exact string removes that error?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const password = form.elements.password;
    const confirm = form.elements.confirm;
    
    function validatePasswords() {
      const message = password.value === confirm.value ? "" : "Passwords must match";
      confirm.setCustomValidity(message);
    }
    
    password.addEventListener("input", validatePasswords);
    confirm.addEventListener("input", validatePasswords);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePick the method that shows the error

      You want the browser to show its own error bubble for a field. Which method do you call?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengePlan a failed submit that everyone can follow

        A submit fails with several errors. Which invalid field should get focus first?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function showError(input, message) {
          const error = document.querySelector("#" + input.id + "-error");
          input.setAttribute("aria-invalid", message ? "true" : "false");
          input.setAttribute("aria-describedby", error.id);
          error.textContent = message;
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Check your understanding

          7 QUESTIONS
          Lesson quiz · 7 questionsScore: first tries count
          1. Question 1 of 7An optional text field has pattern="[A-Z]{3}", and it is empty. What happens with the pattern rule?

            Choose an answer to see the explanation.

          2. Question 2 of 7What does this message check print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const message = "Passwords must match";
            console.log(message === "" ? "valid" : "customError");

            Choose an answer to see the explanation.

          3. Question 3 of 7Which method gives back true or false and fires invalid, but shows nothing to the person?

            Choose an answer to see the explanation.

          4. Question 4 of 7What does this first-flag function print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            function firstFlag(flags) {
              return flags.valueMissing ? "valueMissing" : flags.valid ? "valid" : "other";
            }
            console.log(firstFlag({ valueMissing: true, valid: false }));

            Choose an answer to see the explanation.

          5. Question 5 of 7For which fields is willValidate usually false?

            Choose an answer to see the explanation.

          6. Question 6 of 7Why add novalidate when you show your own error messages?

            Choose an answer to see the explanation.

          7. Question 7 of 7What does this browser-and-server check print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const serverMustCheck = true;
            const browserHelpful = true;
            console.log(browserHelpful && serverMustCheck ? "both" : "browser only");

            Choose an answer to see the explanation.

          Key takeaways

          • HTML rules such as required and pattern give you a lot of validation before you write any JavaScript.
          • validity is a checklist of flags. valid is true only when no rule fails.
          • setCustomValidity("message") turns on customError. setCustomValidity("") turns it off.
          • checkValidity() only tests. reportValidity() tests and shows the browser's error.
          • Your own error messages must work for everyone, and the server must check again.

          Form validation is checking form values against rules. The browser helps people fix mistakes first, and the server makes the final check.

          Up next: FormData.

          CompleteFrontend Clear concepts. Working examples.