Skip to main content

Project 12 · Advanced

Form Validation

A sign-up form that won't let a mistake through. You'll build it twice: once letting the browser check the fields with a few HTML attributes, and once writing the checks yourself, so every message is clear, specific, and read out to screen reader users.

  • 7 HTML ideas
  • 3 CSS ideas
  • 7 JavaScript ideas
  • 2 ways to build it

Why build it

Checking fields and writing clear error messages is what every sign-up, checkout, booking, and contact form needs. With these skills you can build a password field that explains its rules, a check for phone numbers or postcodes, and a form that takes people straight to what's wrong. Clear messages are what turn a form people give up on into one they finish.

Skills you'll earn

  • Check fields with the browser's own rules: required, type, minlength, and pattern
  • Show mistakes only after someone has used a field, with :user-invalid
  • Link each error message to its field so screen readers read it, with aria-describedby
  • Check the shape of text, like a postcode, with regular expressions
  • Take people straight to the first mistake by moving focus to it

01

Try it

Play with it first, then read the code that makes it work.

2 ways to build it

HTML attributes like required, type="email", and minlength do the checking, with almost no JavaScript. The catch: the browser writes the messages, and they're often vague.

Result0px
New tab (opens in a new tab)
Download
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Form Validation</title>
		<link rel="stylesheet" href="style.css" />
	</head>
	<body>
		<main class="card">
			<!-- The browser checks these attributes before it lets the form submit. -->
			<form class="signup" id="signup">
				<h1 class="signup-title">Create an account</h1>

				<div class="field">
					<label for="name">Full name</label>
					<!-- required: the form won't submit while this is empty. -->
					<input id="name" name="name" type="text" autocomplete="name" required />
				</div>

				<div class="field">
					<label for="email">Email</label>
					<!-- type="email" also checks it looks like an email address. -->
					<input
						id="email"
						name="email"
						type="email"
						autocomplete="email"
						required
					/>
				</div>

				<div class="field">
					<label for="password">Password</label>
					<!-- The hint is read with the field, so everyone knows the rules first. -->
					<p class="hint" id="password-hint">
						At least 8 characters, including a number.
					</p>
					<!-- minlength counts characters; pattern needs a digit somewhere. -->
					<input
						id="password"
						name="password"
						type="password"
						autocomplete="new-password"
						minlength="8"
						pattern=".*\d.*"
						aria-describedby="password-hint"
						required
					/>
				</div>

				<button type="submit" class="submit">Create account</button>
			</form>

			<!-- tabindex="-1" lets JavaScript move focus here once it's shown. -->
			<div class="success" id="success" tabindex="-1" hidden>
				<h1 class="signup-title">You're signed up!</h1>
				<p>Check your inbox for a welcome email.</p>
			</div>
		</main>

		<script src="script.js"></script>
	</body>
</html>

02

How it works

The ideas behind it, one at a time, each next to the code that does it.

2 ways to build it

HTML attributes like required, type="email", and minlength do the checking, with almost no JavaScript. The catch: the browser writes the messages, and they're often vague.

HTMLthe structure

  1. 01

    Block empty fields with required

    HTML can check a form before it's sent, using attributes on the inputs. required means the field must have a value. If you press Create account while it's empty, the browser stops the submit, moves focus to the field, and shows its own message beside it. None of this needs JavaScript.

    			<!-- The browser checks these attributes before it lets the form submit. -->
    			<form class="signup" id="signup">
    				<h1 class="signup-title">Create an account</h1>
    
    				<div class="field">
    					<label for="name">Full name</label>
    					<!-- required: the form won't submit while this is empty. -->
    					<input id="name" name="name" type="text" autocomplete="name" required />
    				</div>
  2. 02

    Check emails with type="email"

    type="email" tells the browser the field holds an email address. On submit, it checks the value has the shape of one, like name@example.com, and on phones it shows a keyboard with @ on it. autocomplete="email" says what the field is for, so the browser can offer to fill in a saved address.

    				<div class="field">
    					<label for="email">Email</label>
    					<!-- type="email" also checks it looks like an email address. -->
    					<input
    						id="email"
    						name="email"
    						type="email"
    						autocomplete="email"
    						required
    					/>
    				</div>
  3. 03

    Set password rules with minlength and pattern

    minlength="8" needs at least 8 characters. pattern needs the whole value to match a regular expression, a short code that describes text. Here .*\d.* means "a digit somewhere". If pattern fails, the browser only says "match the requested format", so the hint spells out the rules, and aria-describedby makes screen readers read it with the field.

    				<div class="field">
    					<label for="password">Password</label>
    					<!-- The hint is read with the field, so everyone knows the rules first. -->
    					<p class="hint" id="password-hint">
    						At least 8 characters, including a number.
    					</p>
    					<!-- minlength counts characters; pattern needs a digit somewhere. -->
    					<input
    						id="password"
    						name="password"
    						type="password"
    						autocomplete="new-password"
    						minlength="8"
    						pattern=".*\d.*"
    						aria-describedby="password-hint"
    						required
    					/>
    				</div>
  4. 04

    Make the success message focusable

    tabindex controls whether an element can receive focus. -1 keeps the success message out of the Tab order, so keyboard users don't land on it while filling in the form, but it lets JavaScript move focus there with focus(). When the message appears and gets focus, screen readers read it out.

    			<!-- tabindex="-1" lets JavaScript move focus here once it's shown. -->
    			<div class="success" id="success" tabindex="-1" hidden>
    				<h1 class="signup-title">You're signed up!</h1>
    				<p>Check your inbox for a welcome email.</p>
    			</div>

CSSthe look

  1. 05

    Style mistakes with :user-invalid

    :invalid matches any field that fails its checks, including a required field that's still empty. On a fresh form, every field would look wrong before you'd typed anything. :user-invalid only matches once someone has changed the field or tried to submit, so the red border appears when there's a real mistake.

    /* :user-invalid waits until a field is used; :invalid would flag it at once. */
    .field input:user-invalid {
    	border-color: #b91c1c;
    }

JavaScriptthe behaviour

  1. 06

    Act only on a valid form

    With built-in checks, the browser doesn't fire the submit event until every field passes, so the listener never sees a bad form. preventDefault() stops the page reloading, then the form is hidden, the success message is shown, and focus moves to it. A real site would send the data at this point.

    // "submit" only fires once every field passes the browser's checks.
    form.addEventListener("submit", (event) => {
    	// Stop the page reloading; a real site would send the data here.
    	event.preventDefault();
    	form.hidden = true;
    	successMessage.hidden = false;
    	// Move focus to the message, so screen readers announce it.
    	successMessage.focus();
    });

03

Wrap up

Track your progress, then go deeper or keep going.

Take it further

Once it works, try these on your own. Each one uses what you've just learned, plus one new idea.

  1. Challenge 01

    Show a password strength meter as people type.

  2. Challenge 02

    Add a Show password button that switches the field between hidden and visible.

  3. Challenge 03

    Check the email against a list of addresses that are already taken.

Finished Form Validation?

Mark it done to track your progress.

You finished Form Validation!

0 of 15 projects done.

Go deeper

The full article explains each idea in more depth, why the code is written this way, and more than fits on this page.

Article coming soon. See the series so far (opens in a new tab)