← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 21 — Forms with JavaScript and Validation

Section 1 — What a form is for

A form is how a page collects information from a visitor. A paragraph can tell someone something, a picture can show something, and a link can take someone somewhere, but none of those can ask a question and remember the answer. A form can. It turns a visitor from a reader into a participant: they type their name, choose an option, tick a box, write a message, and send it back to you. That two-way flow is what makes the web useful rather than merely readable. Think of the forms you already use every day. A search box is a form. A login screen is a form. A comment box, a checkout page, a newsletter sign-up, and a contact page are all forms. They look different, but they share the same job: gather input, in a structured way, so a program can do something sensible with it.

The structure matters. A form does not just collect loose text; it collects labelled pieces of data. A name goes in the name field, an email address goes in the email field, and a message goes in the message field. Because each piece has a name and a place, your code can find it again later, check it, store it, or send it to a server. Without that structure you would have a pile of words and no way to tell which word was the name and which was the message. Good forms also guide the visitor. Labels explain what is wanted, hints show the expected format, and error messages explain what to fix. A form that confuses people will be abandoned, however good the rest of the site is, so designing forms well is a core front-end skill, not a side detail.

There is also a difference between a form that merely exists and a form that works well. A working form checks its input before trusting it, tells the visitor clearly when something is wrong, never loses what they already typed when one field fails, and confirms success when everything is right. JavaScript is what adds that behaviour on top of the HTML structure. HTML provides the fields, CSS makes them readable and pleasant, and JavaScript checks, guides, and responds. In this chapter you will learn each layer in turn, starting with the HTML elements themselves, then reading values with JavaScript, then validating them, and finally handling submission as one clean flow. By the end you will be able to build a form that a real visitor can complete without confusion, and that your code can trust enough to use.

A useful habit is to test every form as if you were a tired visitor on a phone, in a hurry, with one hand free. Would you know what each field wants? Would you know what went wrong if you made a mistake? Would you trust that your message was sent? If the answer to any of those is no, the form still needs work, however correct its code looks. Forms are where the front end meets real people most directly, and that is why this chapter treats them with care.

Check: In plain words, what job does a form do that a plain paragraph cannot do?

Section 2 — The form element

All the fields of one form sit inside a single <form> element. That element is a container with a job: it tells the browser that the fields inside it belong together and should be submitted together. When a visitor presses a submit button inside a form, the browser gathers every named field in that form, packages their values, and sends them as one submission. Fields outside the form are ignored, even if they sit nearby on the page. This grouping is invisible to the visitor but essential to the code, because it defines the boundary of one task. A page can hold several forms, and each one is independent. A sign-up form, a search form, and a newsletter form on the same page are three separate forms, each with its own submit button and its own destination.

A common beginner mistake is to put everything on a page inside one giant form, or to forget the form element entirely and scatter inputs across the page with a button that does nothing. Both fail for the same reason: without a form, there is no submission. JavaScript can still read individual fields, but the browser's built-in behaviour, such as submitting when the visitor presses Enter inside a field, will not work, and neither will the built-in validation you will meet later in this chapter. Wrapping related fields in a form costs one pair of tags and buys you all of that behaviour for free.

Consider a contact page. It might have fields for name, email, subject, and message, plus a send button. All five belong in one form, because they are one message. Now consider a page with both a search box at the top and a comment box at the bottom. Those are two different jobs, so they need two forms. If you merged them, pressing the search button might try to send the comment too, or pressing the comment button might send the search term. Keeping forms separate keeps their jobs separate, and that separation makes both your HTML and your JavaScript easier to reason about. When you read a page's code, the form elements tell you at a glance how many tasks the page offers.

There is one more benefit worth knowing early. Because a form groups its fields, JavaScript can treat the form as a unit. You can listen for a submit event on the form, read all its fields at once with FormData, reset the whole form with one call, and check all its fields with one call. All of those conveniences come from the grouping the form element provides. So the rule is simple: one task, one form, with every field for that task inside it, and nothing else.

<form>
  <input name="name">
  <button>Send</button>
</form>

Check: Why do related fields belong inside one form element?

Section 3 — Action and method

Two attributes on the form element describe where its data goes and how it travels. The action attribute is the address the data is sent to, such as /contact or https://example.com/signup. The method attribute says how it is sent, and in practice you will use one of two values: get or post. Understanding the difference between them is important, because choosing the wrong one can expose private data or break a submission entirely. The method is not a decoration; it changes what happens on the network.

With get, the form's data is added to the address bar as a query string. If you search for "cats" on a site, you might see a URL ending in ?q=cats. That is a GET form at work. GET is right for requests that only ask for something and change nothing: searches, filters, and sorting. Because the data sits in the URL, the result can be bookmarked, shared, and revisited, which is exactly what you want for a search. But that visibility is also its weakness. Anything sent with GET appears in the address bar, in browser history, and in server logs. Passwords, personal details, and long messages should never travel that way.

With post, the data travels inside the body of the request, not in the URL. The visitor sees a clean address, and the data is not left lying around in history in the same way. POST is right for anything that creates or changes something: sending a message, creating an account, placing an order, or saving a setting. It also has no practical limit on length in the way a URL does, so long messages belong in POST requests. If you are ever unsure, ask a simple question: would it be safe and sensible for this submission to be bookmarked and shared as a link? If yes, GET fits. If the submission changes something, contains anything private, or is long, choose POST.

There is a practical detail for this book's exercises. When you handle a form entirely with JavaScript, you will often prevent the browser's default submission, as you will learn in Section 21, and send the data yourself with fetch, covered in Chapter 22. In that case the action and method still matter as a fallback: if your JavaScript fails to load, the browser can still submit the form the old-fashioned way. Setting them correctly, even when JavaScript takes over, is therefore good defensive practice. It also documents your intention for the next person who reads the code.

<form action="/contact" method="post"></form>

A useful test for choosing a method is to imagine the submission as a link. If you would happily email that link to a colleague, and opening it should simply show a result without changing anything, GET is appropriate. If opening it again would send a second message, create a second account, or charge a second time, it must not be a GET, because links are meant to be safe to revisit. That test captures the web's own rule: safe, repeatable requests use GET, and actions that change the world use POST.

Check: When would you choose post instead of get?

Section 4 — Text inputs

The most common field on the web is the text input, written as <input type="text" name="name">. It gives the visitor a single line in which to type short, free-form text: a name, a town, a job title, a username. The input element is unusual in HTML because it has no closing tag and no content between tags; everything about it is expressed through attributes. The type attribute says what kind of data it holds, the name attribute gives it an identity for submission, and the id attribute lets a label and your JavaScript point at it. Those three attributes together make a text input fully usable, and you will use them on almost every field in this chapter.

Single-line matters. A text input is for short answers, not essays. If the visitor needs to write several lines, a textarea, covered in Section 12, is the right tool. Forcing a long message into a single-line input makes it hard to review, because only part of it is visible at once, and it signals to the visitor that a short answer is expected. Matching the field to the length of the answer is a small design decision that prevents a lot of frustration. Similarly, do not use a plain text input for data with a known shape when a more specific type exists. Email addresses, numbers, dates, and web addresses each have their own input types, covered in the next few sections, and each one brings helpful behaviour: a better phone keyboard, a calendar picker, or a basic shape check.

There are a few extra attributes worth knowing for text inputs. maxlength caps how many characters can be typed, which suits codes and short identifiers. autocomplete hints to the browser what the field is for, such as a name or an organisation, so it can offer to fill it in. readonly lets a visitor see a value but not change it, while disabled greys the field out and, importantly, excludes it from submission altogether. That last point surprises people: a disabled field's value is not sent, so never disable a field whose value you still need. If you want a value shown but unchangeable and still submitted, readonly is the choice.

When you read a text input in JavaScript, you get a string, even if the visitor typed digits. That is fine for names, but it matters for numbers, as Section 32 explains. For now, remember the core idea: a text input is the default tool for short, free-form text, it is defined by its attributes rather than by content, and choosing a more specific type whenever one fits will make your forms easier to use and easier to validate.

A text input's width should also hint at the expected answer length. A narrow field suggests a short code, while a wide field suggests a name or a title, and visitors unconsciously adjust their answers to the space provided. Sizing a field to its expected content is therefore a quiet form of guidance, helping visitors give answers of the right length without any extra instructions.

Check: What does the type attribute of an input control?

Section 5 — Labels that actually label

Every field needs a label, and the label must be properly linked to its field. The link is made with two matching attributes: the label's for attribute and the field's id attribute. When they match, the browser treats the label and the field as one unit. Clicking the label focuses the field, which is a real help on small screens where a field itself can be a fiddly target, and on checkboxes and radio buttons, where clicking the words is far easier than clicking the tiny box. For visitors using screen readers, a linked label is even more important: the screen reader announces the label when the field receives focus, so the visitor knows what the field is asking for without being able to see the layout.

There are two ways to link a label. The explicit way, shown below, uses for and id, and it works wherever the label and field sit in the document. The implicit way wraps the field inside the label element itself, which also links them, and is handy for checkboxes where the text sits right beside the box. Both are valid, but the explicit way is easier to keep correct in larger forms, because the label and the field can be laid out independently, and your JavaScript can also use the same id to find the field. Whichever way you choose, the test is simple: click the label text. If the field gains focus, or the checkbox toggles, the link works. If nothing happens, it does not.

A label should also say plainly what is wanted. "Email address" is a good label. "Info" is not, because it does not tell the visitor what information, or in what format. If a field has a rule, such as a minimum length, put that rule where the visitor can see it before they type, either in the label itself or in a short hint beside the field. Do not hide the rules and then punish the visitor with an error for breaking a rule they were never told. Good labels also avoid relying on colour or position alone to carry meaning. A red asterisk may mean "required" to a sighted visitor, but it means nothing to a screen reader unless the word "required" is present too. Writing the word costs nothing and includes everyone.

Finally, remember that placeholder text is not a label, a point Section 7 makes in full. A form can look clean and minimal with placeholders alone, but it fails the moment a visitor starts typing, because the only explanation disappears. Labels stay visible. They are the permanent, clickable, announced description of each field, and no form is complete without them.

<label for="email">Email address</label>
<input id="email" name="email" type="email">

Labels also help when a form is translated or restyled, because the label text travels with the field as a unit, rather than being a separate piece of layout that might be lost. A form whose labels are properly linked will survive a redesign, a theme change, or a move to a new framework with its meanings intact, while a form that relies on nearby text or placeholder hints may lose those meanings when the layout changes.

Check: What two things does a correctly linked label make possible?

Section 6 — The name attribute

The name attribute is a field's identity when its data is sent or read, and it is easy to underestimate because it has no visible effect on the page. A visitor never sees a name attribute, yet without one, a field's value may never arrive anywhere. When a form is submitted the old-fashioned way, the browser builds its submission from name-and-value pairs: each field contributes its name and whatever the visitor typed. A field with no name contributes nothing, no matter how carefully the visitor filled it in. The data is silently dropped, which makes this one of the more frustrating bugs to diagnose, because the form looks perfect on screen while quietly losing information.

The same identity matters in JavaScript. FormData, covered in Section 22, collects values by name, and form.elements lets you reach a field by its name. When your HTML, your JavaScript, and your server all agree on the same names, the whole flow reads clearly: the field named email in the HTML is data.get("email") in the script and the email value on the server. When names drift apart, perhaps emailAddress in one place and user_email in another, bugs hide in the gaps. Choose names early, keep them short and consistent, and use the same spelling everywhere. Lowercase words joined simply, such as firstName or phone, are easier to keep consistent than names with spaces or mixed punctuation.

Names also have rules worth knowing. Two fields can share a name deliberately, and that is how groups work: radio buttons in one group share a name so the browser knows they are alternatives, and checkboxes for a list of interests can share a name so all the ticked values arrive together. Outside those cases, give each field its own name, so each value can be found unambiguously. A duplicate name on two unrelated text fields would make their values collide, and your code would have to guess which was which. Another point: the name and the id are different attributes with different jobs. The id links a label and lets JavaScript select one element on the page; the name identifies the field's data in a submission. They often hold similar values, and that is fine, but they are not interchangeable. A field can have an id without a name, in which case it is labellable but not submittable, or a name without an id, in which case it submits but cannot be labelled in the explicit way. A complete field has both, each doing its own job, and checking for both is a quick review habit that catches a whole class of silent form bugs before a visitor ever meets them.

Names should also be chosen with the server in mind, because the names in a form often become the keys in the data the server receives. A name that is clear in the form, such as email, will be clear in the server's data too, while a vague name, such as field1, will confuse everyone who works with that data later. Choosing names as if they will be read by strangers is good practice, because they eventually will be.

Check: What happens to a field's value if the field has no name?

Section 7 — Placeholder is not a label

Placeholder text is the faint hint that sits inside an empty field, such as "you@example.com" in an email field or "Search products" in a search box. It is useful, but only within narrow limits, and the most common form mistake is treating it as a replacement for a label. The problem is timing: placeholder text disappears the moment the visitor starts typing. At exactly the point where they might want to double-check what the field was asking for, the explanation is gone. A visitor who is interrupted mid-form, or who returns to a half-filled field, has no way to recover what the placeholder said without clearing the field and losing their work. Labels, by contrast, stay visible for the whole interaction.

Placeholder text has other weaknesses too. Its default colour is pale grey, which can be hard to read, especially in bright light or for visitors with low vision. It is also not reliably announced by screen readers in the same way a label is, so a visitor who cannot see the screen may never hear the hint at all. And because it sits inside the field, it can be mistaken for a value that is already filled in, leading a visitor to skip a field they thought was complete. For all these reasons, accessibility guidance treats placeholder as a hint, never as the field's name. The label names the field; the placeholder, if used at all, shows an example of the answer's shape.

Used properly, placeholders are genuinely helpful. An email field labelled "Email address" with the placeholder "you@example.com" shows the expected format without adding clutter. A date field might show "DD/MM/YYYY" as a reminder of the order, though a date picker input avoids the question entirely. A search box might show "Try 'headphones'" as a suggestion. In each case the placeholder adds an example, while the label carries the meaning. The test is straightforward: remove every placeholder from your form. If the form is still fully understandable, the placeholders were doing their proper, supporting job. If removing them leaves a mystery field, the form was depending on them, and it needs a real label. Keep placeholders short, never put essential instructions only in a placeholder, and never put a password hint or any sensitive example in one. A hint that vanishes, fades, and may never be heard is a poor place for anything important.

A practical check is to fill a form, then look away and look back. Can you still tell what each filled field was for? If a field contains only its own value, with no label nearby, you are relying on memory, and a visitor returning to correct a mistake faces the same problem. Labels solve it permanently, because they remain visible beside the filled value, reminding the visitor what each answer means, long after the placeholder has gone.

Check: Why should placeholder text never be the only description of a field?

Section 8 — Password inputs

A password input, written as <input type="password">, looks like a text input but hides what is typed behind dots or stars. Its purpose is narrow and practical: it stops someone glancing at the screen, or looking over the visitor's shoulder, from reading the password as it is typed. That is a real protection in a café, a classroom, or an office, where screens are visible to others. It is also the limit of what this input type does, and understanding that limit matters. A password input does not encrypt the password, does not store it safely, and does not make the submission secure on its own. The characters are hidden on screen, but the value itself is still ordinary text in the page and in the request, and it is only protected in transit if the page is served over HTTPS, which encrypts the whole conversation between browser and server.

Good password fields add a few thoughtful touches. Many sites offer a "show password" toggle, a button that switches the input between password and text so the visitor can check what they typed. This reduces failed logins caused by typos the visitor could not see, and it is worth implementing once you are comfortable with changing attributes in JavaScript: toggling the type attribute is all it takes. The autocomplete attribute also helps here. Setting it to current-password on a login form or new-password on a sign-up form tells the browser and password managers what the field is for, so they can offer to fill it or to save a new password. Visitors who use password managers have stronger, unique passwords, and helping those tools work is helping your visitors stay safe.

There are also things a password field should never do. Never display a password in plain text elsewhere on the page, never log it to the console, and never send it to analytics or error reporting. During development it is tempting to console.log a form's values to check they were read correctly, but a password in a console log is a password lying around in a place it does not belong. When you test a login or sign-up form, use a made-up password and still avoid printing it. The same caution applies to password confirmation fields, covered in Section 31: compare the two values in code, show only whether they match, and never echo either value back. A password input hides characters for a reason, and the rest of your code should respect that reason throughout.

Another practical point is that a password field should never be pre-filled with a real password in HTML, and it should not be given a value attribute containing one, because that value would be visible in the page source to anyone who inspects it. A password field starts empty, the visitor types into it, and the browser's password manager, not the page's HTML, is the tool that remembers it for next time. Keeping passwords out of source code and out of logs is a basic security habit.

Check: Does a password input encrypt the password? Why or why not?

Section 9 — Email and URL inputs

Email and URL inputs look like ordinary text inputs, but their types tell the browser what shape of data to expect, and the browser responds with small, practical helps. <input type="email"> marks a field as an email address, and <input type="url"> marks one as a web address. The most visible benefit appears on phones, where the keyboard changes to suit the task: an email field brings up a keyboard with an @ sign and a dot close at hand, while a URL field offers a dot, a slash, and often a ".com" key. These are small kindnesses, but on a phone keyboard every saved tap counts, and a visitor who does not have to hunt for the @ sign is a visitor less likely to abandon the form.

The second benefit is validation. The browser knows the basic shape of an email address: some text, an @ sign, more text, a dot, and more text. If a visitor submits an email field containing "not-an-email", the browser can block the submission and say the address is incomplete, before your JavaScript runs at all. The same applies to URLs, which must look like a web address. These checks are deliberately shallow. They catch missing @ signs and missing dots, but they cannot tell whether the address belongs to anyone, whether the mailbox exists, or whether the visitor typed their own address rather than someone else's. Section 29 returns to that limit in detail. For now, treat built-in type checks as a first net: useful, cheap, and not sufficient on their own.

Choosing the right type also communicates meaning to other software. Browsers can offer autofill for email fields, password managers recognise them, and assistive technology can announce the expected kind of input. A field typed as plain text gives none of those hints, so software must guess. There is a related point about multiple addresses. An email input can accept a list of addresses, separated by commas, if you add the multiple attribute, which suits a field for inviting several people at once. Without it, one address is expected. Similarly, a URL input expects a complete address including the scheme, such as https://example.com, and some browsers will flag an address without it. If your form asks for a website, say so in the label or hint, for example "Website, starting with https://", so visitors are not caught out by a rule they were not told. Picking the specific type instead of generic text is a one-word change with benefits for keyboards, validation, autofill, and accessibility, which is why it is worth making a habit of it from the start.

A further benefit appears when a form is submitted with JavaScript disabled. The browser's built-in checks for email and URL fields still run, blocking a badly shaped address before it is sent, which means a page that uses specific types degrades gracefully when scripts fail. A page built entirely from text inputs loses that safety net, and a badly shaped address would travel to the server unchecked. Specific types therefore protect the form in both the scripted and the unscripted cases.

Check: Name one practical benefit of using type="email" instead of type="text" for an email field.

Section 10 — Number inputs

A number input, <input type="number">, is for fields where the answer is a number: an age, a quantity, a price, a rating. On desktop browsers it often shows small up and down arrows, called spinners, for stepping the value up or down by one. On phones it brings up a numeric keyboard, which is far easier than a full keyboard for entering digits. Those conveniences are the main reason to choose it over a text input for numeric answers. But the number input comes with quirks that you need to know before you trust it, because it behaves less simply than it first appears.

The first quirk is that its value, read in JavaScript, is still a string. Section 19 makes this point generally, but it bites hardest with numbers, because a string "5" plus a string "5" is "55", not 10. Always convert with Number() before doing arithmetic, and check the result is not NaN, as Section 32 explains. The second quirk is that a number input's allowed values can be controlled with min, max, and step. Setting min="1" and max="10" says the value should fall in that range, and step="2" says allowed values step by two from the minimum. The browser will flag values outside the range, but a determined visitor can still type or paste an out-of-range value in some browsers, and older browsers may not enforce the range at all. So min, max, and step are guidance and a first check, not a guarantee, and your JavaScript must re-check the range.

A third quirk concerns what counts as a number. A number input rejects letters in most browsers, but it accepts scientific notation, negative signs, and decimal points, so "1e3" may be accepted as 1000 where you expected a plain quantity. If you need a strictly whole number of a particular format, such as a four-digit code, a text input with a pattern, covered in Section 41, may serve you better than a number input. Similarly, numbers with leading zeros, such as a postal code or a phone number, are not really numbers for this purpose: the leading zero matters, and a number input would drop it. Phone numbers, codes, and identifiers should be text inputs, even though they contain digits. The rule of thumb is this: use a number input when the value will be used as a number, in arithmetic or comparison, and a text input when the digits are really an identifier or a code whose exact characters matter.

When a number input is used for a quantity, consider also what should happen at the boundaries. A quantity of zero may mean the visitor wants nothing, which could be a valid choice or a mistake, depending on the task, and a negative quantity is almost never valid for a basket. Stating the allowed range in the label, such as 'Quantity (1 to 10)', and checking it in JavaScript, as Section 32 shows, removes ambiguity about whether zero or a negative number was intended, before the visitor submits.

Check: If a number input has min="1" max="10", what values should it accept?

Section 11 — Date and time inputs

Dates are a classic source of form confusion, because people write them in different orders around the world. In one country 03/04/2026 means 3 April; in another it means March 4. A free text field asking for a date invites exactly that ambiguity, and your code is left guessing which order the visitor meant. A date input, <input type="date">, removes the problem by giving the visitor a calendar picker instead of a blank box. They tap a day on a calendar, and the browser returns the date in a single, unambiguous format: year first, then month, then day, as in "2026-10-29". Your code never has to parse "March 4" or "4/3" or "04-03-26", because the visitor never typed any of those.

The returned format is worth pausing on, because it is also a sensible format for comparing and storing dates. Year-month-day order sorts correctly as plain text: "2026-10-29" comes before "2026-11-01" both alphabetically and chronologically, which is not true of day-first or month-first formats. You can also turn the string into a Date object with new Date(value) when you need to compare dates or calculate the gap between them, as Section 33 shows. Related types cover the neighbouring needs: time for a time of day, datetime-local for a date and time together without a time zone, week for a week of the year, and month for a month and year. Each one gives the visitor a picker suited to that kind of value, and each returns a standard format your code can rely on.

Date inputs also accept min and max attributes, which limit the calendar to a sensible range. A booking form can set its minimum to today, so past dates cannot be picked, and a date-of-birth field can set a maximum of today, since nobody is born in the future. As with number inputs, these limits guide the visitor and the browser's built-in checks, but they are not a substitute for checking in JavaScript, because a visitor can still type a date directly in some browsers, and the limits themselves need to be kept up to date: a hardcoded minimum of today becomes yesterday's minimum tomorrow. Setting the minimum dynamically in JavaScript, to the current date, is a neat exercise once you know how to build a date string. The broader lesson is that whenever a value has a known shape, a specialised input that enforces that shape is better than a blank box and a hope. Dates, with their competing formats and easy mistakes, are one of the clearest examples of that principle.

For a date of birth field, a date input also prevents impossible dates, such as 31 February, which a text field would accept and your code would then have to reject. The calendar picker only offers real calendar days, so the visitor cannot select a day that does not exist. That prevention is more helpful than a correction after the fact, because the visitor never makes the mistake in the first place, and your code never has to explain why a date that looked plausible was rejected.

Check: In what order does a date input return its value?

Section 12 — Textareas

A textarea is the field for longer text: a message, a comment, a description, a review. It is written as <textarea name="message" rows="4"></textarea>, and unlike an input, it has an opening and a closing tag, with any starting text placed between them. Most of the time that starting text is empty, and the visitor fills it. The rows attribute sets its visible height in lines of text, and cols sets its visible width in characters, though CSS usually controls the width in real projects. A height of four or five rows suits a short message; a longer piece of writing deserves more. The key point is that the visitor can see several lines at once, scroll within the box, and review what they wrote, which a single-line input does not allow.

Textareas share the same rules as other fields. They need a label linked with for and id, they need a name so their value is submitted, and their value is read in JavaScript with .value, just like an input's. They also accept several familiar attributes: maxlength caps the number of characters, placeholder can show an example, and required makes the field mandatory. A character count, updated on the input event as in Section 24, is a particularly friendly companion for a textarea with a limit, because it tells the visitor how much room remains before they hit the cap, rather than letting them type freely and then rejecting the whole message at submission. Showing "120 of 500 characters" as they type is guidance; showing only an error at the end is a reprimand.

There is one behaviour of textareas worth knowing: by default, visitors can drag the corner of a textarea to resize it in most browsers. That is usually a good thing, since it lets someone make room for a long message, but it can break a carefully designed layout if the box is dragged very wide. CSS can control this with the resize property, for example resize: vertical to allow only height changes, but that is a styling decision for a later chapter. What matters here is the choice of element. If the answer might run to more than a line, give it room. A message field squeezed into a single-line input tells the visitor, wrongly, that a few words will do, and it makes reviewing a longer answer needlessly hard. Matching the field's shape to the answer's likely length is a small courtesy that makes forms feel considered rather than careless.

<label for="message">Message</label>
<textarea id="message" name="message" rows="4"></textarea>

A textarea's size should also match the task's expectations. A message box only two lines high signals that a short note is expected, while a box ten lines high invites a longer, more thoughtful message. Visitors take cues from the space they are given, so sizing a textarea is not merely a styling choice; it communicates how much detail you hope for. If you expect a paragraph, give room for a paragraph, and if you only need a sentence, a smaller box says so without words.

Check: When should you choose a textarea instead of a text input?

Section 13 — Select dropdowns

A select element offers a fixed list of choices in a dropdown. The visitor clicks, sees the options, picks one, and the field's value becomes the value attribute of the chosen option, not necessarily the visible text. That distinction matters: you can show "United Kingdom" to the visitor while sending the short code "GB" to the server, by writing <option value="GB">United Kingdom</option>. If an option has no value attribute, its visible text becomes its value. Either way, the value is what your code and the server receive, so choose values that are stable and easy to work with, such as short codes or simple words, rather than long sentences that might change.

Dropdowns are the right tool when the options are known in advance and limited in number: a country, a ticket type, a size, a category. They prevent a whole class of errors that free text invites. Nobody can misspell a country name in a dropdown, because they can only pick from the list, and your code never has to guess whether "USA", "U.S.A.", and "United States" were meant to be the same thing. They also guide visitors who might not know the exact wording you expect. A text field asking for a ticket type leaves the visitor guessing what types exist; a dropdown shows them. For very long lists, such as every country in the world, a dropdown can become tedious to scroll, and a text field with suggestions or a searchable list may serve better, but for short and medium lists the dropdown is hard to beat for simplicity and reliability.

A few practical details complete the picture. The first option can act as a prompt, such as "Choose a ticket type", with an empty value, so that a required dropdown cannot be submitted with the prompt still selected. The multiple attribute turns a select into a list box where several options can be chosen at once, though that interface is clumsy on many devices and checkboxes are usually friendlier for multiple choices. The size attribute controls how many options are visible without opening the dropdown. And in JavaScript, a select's value is read with .value, just like a text input's, while the selected option's visible text can be found through the select's options if you need it for display. When the choices are fixed, let the visitor choose rather than type; the form will be faster to complete, and the data will arrive clean.

<select name="ticket">
  <option value="standard">Standard</option>
  <option value="student">Student</option>
</select>

When a dropdown's options change depending on an earlier choice, for example cities that depend on a chosen country, the second dropdown should be rebuilt or disabled until the first choice is made. Showing cities for every country at once, before a country is chosen, forces the visitor to guess which ones apply, and it risks an inconsistent combination, such as a city from one country paired with another. Dependent dropdowns, updated in JavaScript when the first choice changes, keep combinations valid by construction.

Check: Why is a dropdown better than a text field for choosing a ticket type?

Section 14 — Checkboxes

A checkbox is a small box that is either ticked or unticked, representing one yes-or-no choice. Unlike a text field, its meaning is not in what the visitor typed but in its state: checked means yes, unchecked means no. That state is read in JavaScript with the .checked property, which is either true or false, rather than with .value. The .value of a checkbox is a different thing: it is the value sent when the box is ticked, defaulting to "on" if you do not set one, and nothing is sent at all when the box is unticked. This distinction trips up beginners who read .value expecting true or false and get "on" instead. For a single agreement checkbox, .checked is what you want; for a group of checkboxes sharing a name, the submitted values of the ticked ones are what you want.

Checkboxes shine for "tick all that apply" lists. A visitor choosing interests, newsletter topics, or pizza toppings can tick as many as they like, because each checkbox is independent. That independence is the key difference from radio buttons, covered next, where picking one excludes the others. When you lay out checkboxes, wrap each one with its label, or link label and box with for and id, so clicking the words toggles the box. A checkbox itself is a small target, and on a phone the difference between tapping a tiny box and tapping a whole line of text is the difference between a form that feels easy and one that feels fiddly. Making the label clickable costs nothing and helps everyone.

One more behaviour deserves attention: an unticked checkbox sends nothing. If a form is submitted the old-fashioned way, the server simply receives no entry for that checkbox, and must interpret absence as "no". In JavaScript, that is rarely a problem, because you read .checked directly and get a clear false. But it explains why a required agreement checkbox works differently from a required text field. You cannot make a checkbox "required" in the text sense; you check its .checked state in your validation, and if it is false, you show an error saying the terms must be accepted. Used for agreements, confirmations, and optional extras, checkboxes are the simplest way to capture a yes-or-no decision, provided you read their state, not their text, and make their labels generous targets.

A checkbox group for optional extras also benefits from a clear group label, such as a fieldset legend saying 'Choose any extras you would like'. Without that heading, a run of checkboxes can look like unrelated questions, and a visitor may not realise they can tick several. With it, the task is obvious: these are options, pick as many as you want. That clarity is especially helpful for visitors using screen readers, who hear the legend as context for each box in the group.

Check: How do you read whether a checkbox is ticked in JavaScript?

Section 15 — Radio buttons

Radio buttons are for choices where exactly one option must be picked. They look like small circles, and selecting one fills its circle while emptying the others in the same group. The grouping is created by the name attribute: every radio button with the same name belongs to one group, and the browser enforces that only one of them can be selected at a time. Give each button in the group a different value, so your code can tell which option was chosen. If you accidentally give two groups the same name, they merge into one group and picking an option in one will deselect an option in the other, which is a bewildering bug for a visitor. If you give buttons in one group different names, the browser treats them as separate groups and lets the visitor pick them all, defeating the purpose entirely. The name is the group, so get it right.

Radio buttons suit short lists of mutually exclusive options: a T-shirt size, a delivery method, a payment option, a yes-or-no question where an explicit answer is required. For two options, radio buttons are often clearer than a dropdown, because all the choices are visible at once and the visitor can compare them without opening anything. For longer lists, a dropdown or a select may be tidier, but radio buttons keep every option in view, which helps when the visitor needs to weigh them. As with checkboxes, wrap each button with its label, or link them with for and id, so clicking the words selects the button. A radio button itself is a tiny target, and a form that forces precise taps on tiny circles feels hostile on a phone.

Reading a radio group in JavaScript takes one extra step compared with a single field, because the group has several elements but only one value: the value of the checked button. The simplest approach is to ask the form for the checked button in the group, for example with a selector that combines the name and the checked state, and then read its value. If none is checked, there is no value, which is the case your validation must handle for a required choice. A required group with no default selection is usually the honest design: preselecting an option can lead visitors to submit a choice they never actually made, especially on questions like payment method. Letting the visitor choose, and validating that they did, respects their decision and keeps your data truthful.

<label><input type="radio" name="size" value="small"> Small</label>
<label><input type="radio" name="size" value="large"> Large</label>

When none of the radio options fits, a well-designed group sometimes includes an 'Other' option with a text field that appears when it is selected. That combination preserves the exactly-one rule while giving the visitor an escape hatch for an answer you did not anticipate. Without it, a visitor whose true answer is not listed must either pick a wrong option or abandon the form, and either outcome corrupts your data. An 'Other' option, used sparingly, keeps data honest without turning the group into a free text field.

Check: What makes several radio buttons act as one group?

Section 16 — Fieldset and legend

A fieldset groups related fields together, and its legend gives that group a title. In HTML you write a <fieldset> element, put a <legend> as its first child, and then place the grouped fields inside. Visually, browsers traditionally draw a border around the fieldset with the legend sitting on the border, like a heading for the box, though CSS can restyle that freely. The visual grouping is helpful, but the real value is structural and accessible. A screen reader announces the legend as the visitor moves through the fields in the group, so each field is understood in context. A field labelled "City" inside a fieldset whose legend is "Delivery address" is announced with that context, which removes ambiguity when a page has two city fields, one for billing and one for delivery.

Fieldsets are natural for any set of fields that belong together. An address block, with street, city, and postal code, is one group. A set of radio buttons for delivery method is another: the legend asks the question, "Delivery method", and each radio button is an answer. A payment section, a set of contact preferences, or a group of checkboxes for interests all fit the same pattern. Without a fieldset, those fields are just a run of labels on a page, and a visitor using a screen reader must remember which section they are in from the headings alone. With a fieldset, the grouping is part of the document's structure, not just its appearance, so it survives restyling, zooming, and assistive technology.

There are two practical points to keep in mind. First, a fieldset can be disabled as a whole: adding disabled to a fieldset disables every field inside it, which is convenient for sections that only become relevant after an earlier choice, such as delivery details that only matter when home delivery is selected. Remember, though, that disabled fields are not submitted, as Section 4 noted. Second, do not overuse fieldsets. Wrapping every single field in its own fieldset adds noise without meaning, and a page of nested boxes becomes harder to scan. Use a fieldset when the fields genuinely form a set that shares a question or a context, and give the legend a clear, short title that states that context. One group, one question, one legend: that is the shape of a well-grouped form, and it is a habit that makes both your HTML and your visitors' experience clearer.

Fieldsets also help sighted visitors scan a long form, because the border and legend break the form into visual chunks, each with a heading, rather than a single unbroken run of fields. A visitor looking for the payment section can find its legend and jump to it, instead of reading every label in order. That chunking mirrors how people read forms: they look for the section they need, complete it, and move to the next, and a structure that supports that scanning is easier to complete than a flat list.

Check: What does a legend describe?

Section 17 — Submit buttons

A submit button is the control that sends a form. In HTML it is a button with type="submit", written as <button type="submit">Send message</button>, or an input with type="submit" and a value for its label. When clicked, it triggers the form's submit event, which is where your validation lives, and, unless JavaScript prevents it, causes the browser to send the form to its action address. The button's text should describe the action in the visitor's terms: "Send message", "Create account", and "Place order" are good; a bare "Submit" is vague, and "Click here" says nothing about what will happen. The button is also the visible promise of the form: its label tells the visitor what pressing it will do, so write it as a promise you intend to keep.

There is a default behaviour that catches many beginners. A plain <button> with no type, placed inside a form, is treated as a submit button by the browser. That means a button you added for a different purpose, such as showing extra help or adding another row to a list, will unexpectedly submit the form when clicked, reloading the page or navigating away and possibly losing the visitor's work. The fix is simple and should become a reflex: every button that is not meant to submit gets type="button" written explicitly. The three types, submit, button, and reset, are compared side by side in the next section, but the rule to carry forward is this: never leave a button's type to chance inside a form. Say what you mean, on every button, every time.

A submit button also participates in another piece of browser behaviour worth knowing: pressing Enter while focused in a text field inside a form usually submits the form, as if the submit button had been clicked. This is why your submit handler must live on the form's submit event, not on the button's click event, a point Section 20 makes in full. A form whose only submission path is a button click will miss Enter-key submissions, and a visitor who submits that way will bypass your validation entirely. A clearly labelled submit button, an explicit type on every other button, and a handler on the form itself are three small decisions that together make submission reliable. They cost a few words of HTML and prevent a class of bugs that are otherwise hard to reproduce, because they only appear when a visitor uses the keyboard rather than the mouse.

The submit button's position also matters. Placing it at the end of the form, after the last field, matches the visitor's reading order: they complete the fields in order and arrive at the button when they are ready. Placing it at the top, or far from the fields, forces them to hunt for it, and a visitor who cannot find how to send a completed form may assume the form is broken. A button that follows naturally from the last field, clearly labelled, is the easiest possible finish to a form.

Check: What does a plain button inside a form do unless you say otherwise?

Section 18 — Button types, side by side

Buttons in a form come in three types, and choosing between them is choosing the button's job. The first, submit, sends the form. It fires the form's submit event, runs your validation, and, unless prevented, navigates to the form's action. Every form that is meant to be sent needs exactly one primary submit button, labelled with the action it performs. The second, button, does nothing on its own. It waits for your JavaScript to tell it what to do, which makes it the right choice for secondary actions inside a form: showing or hiding a section, adding another item to a list, toggling a password's visibility, or clearing a single field. A button with type="button" will never accidentally submit the form, which is why it is the safe default for any button whose job you are defining in code.

The third type, reset, clears every field in the form back to its starting value. That sounds useful, but in practice it is a dangerous button to offer. A visitor who clicks it by mistake, perhaps reaching for the submit button beside it, loses everything they typed, with no way to undo it. There is no confirmation, no undo, and often no warning. For that reason, reset buttons are rarely a good idea on real forms. If you need a way to clear a form, a deliberate action, such as a link that asks for confirmation, or simply letting the visitor edit their entries, is kinder. The main exception is a search or filter form, where clearing the filters to start again is a genuine, low-cost task, and even there the button should be clearly labelled, such as "Clear filters", and kept away from the submit button.

Comparing the three types makes the design principle clear. Submit is for finishing the form's task. Button is for everything else you define in JavaScript. Reset is for starting over, and starting over should be hard to do by accident. A useful review habit is to scan every button in a form and ask, for each one, "What type is this, and is that the job I intend?" If the answer is "I did not specify", the browser has specified it for you, and inside a form that specification is submit. Explicit types remove that guesswork. They also make your code easier for the next developer to read, because the HTML itself states each button's purpose instead of leaving it to be inferred from the JavaScript that happens to listen to it.

A useful review exercise is to open a form you have built, list every button in it, and write down each button's type and its intended job. If any button's type is missing, the browser has decided its job for you, and inside a form that decision is submit. That audit takes a minute and catches the most common button bug, a secondary button that unexpectedly submits, before a visitor meets it. Making the audit a habit, for every form, is cheaper than debugging a lost form submission afterwards.

Check: Which button type should you use for a button that only runs your JavaScript?

Section 19 — Reading a value with JavaScript

Reading a field's value in JavaScript is the foundation of everything else in this chapter, and it rests on two steps: find the field, then read its .value. To find it, give the field an id in the HTML and select it with document.querySelector("#name"), or use the form's elements collection, covered in Section 23. Either way you end up with a reference to the field element itself. Reading .value on that reference gives you whatever the visitor typed or selected, as a string. That string is the raw material for validation, for display, and for sending to a server. The example below shows the simplest version: find the field, read the value, and log it so you can see it in the console while you are learning.

const name = document.querySelector("#name").value;
console.log(name);

There are three details that matter from the start. First, the value is always a string, even when the field is a number input, a date input, or a range slider. A number input containing 42 gives you the string "42", not the number 42. For display that makes no difference, but for arithmetic it matters enormously, as "42" + 1 is "421" in JavaScript's string concatenation. Convert with Number() when you need a number, and with a Date object when you need a date. Second, the value reflects the field's current state at the moment you read it. If you read it when the page loads, before the visitor has typed anything, you will get an empty string. That is why values are read inside event handlers, at submit time or on input, rather than once at the top of the script. Reading early and reusing a stale value is a common bug: the code checks a snapshot taken before the visitor typed, and concludes the field is empty forever.

Third, different field types expose their state differently. Text inputs, textareas, selects, and date inputs all use .value. Checkboxes and radio buttons use .checked for their state, with .value meaning something else, as Sections 14 and 15 explain. Files use a files list, because a file input can hold one or more files that are not strings at all. Knowing which property to read for each field type is part of the job, and the documentation for each input type will tell you. The habit to build is simple: select the field, read the right property, at the right moment, inside the handler that needs it. Everything in the rest of this chapter, from trimming to validation to submission, is built on that one operation done correctly.

A practical tip for reading values is to log the value and its type while you are learning, using console.log with typeof, so you can see for yourself that a number input gives a string. That habit builds an accurate mental model faster than reading about it, because you observe the behaviour in your own page. Once the model is firm, you will convert values without thinking about it, but while you are learning, checking the type explicitly prevents a whole class of arithmetic and comparison bugs.

Check: What type of value does .value always return, even from a number input?

Section 20 — The submit event

When a form is submitted, it fires a submit event. That event can be triggered in several ways: clicking a submit button, pressing Enter while focused in a text field, or calling the form's requestSubmit() method in code. All of those paths fire the same event on the same element, the form itself, which is why the submit event is the right place to put your handling logic. Listen for it on the form, not on the button. A handler on the button's click event would miss Enter-key submissions entirely, and a visitor who submits with the keyboard would sail past your validation. A handler on the form catches every route to submission, which is exactly what a safety net needs to do.

form.addEventListener("submit", (event) => {
  // check the fields here
});

The event object passed to your handler is your control panel for the submission. Its most important method is preventDefault(), covered in the next section, which stops the browser's default submission so your code can take over. The event also gives you access to the form itself, through event.target or through the form variable you already hold, so you can read its fields, check them, and decide what happens next. A typical handler follows a fixed shape: prevent the default, read the values, validate them, show any errors and stop if there are any, and otherwise proceed to use or send the data. That shape appears again in Section 50 as the chapter's summary flow, and it is worth memorising, because it is the shape of almost every form handler you will ever write.

There is one subtlety worth knowing. A form also has a submit() method, but calling it directly does not fire the submit event and does not run validation; it submits immediately, bypassing your handler. The newer requestSubmit() method, by contrast, behaves like a visitor pressing the submit button: it fires the submit event and runs validation first. If you ever need to submit a form from code, for example after a confirmation step, use requestSubmit() so your handler still runs. The distinction is a small one, but it illustrates a broader principle of this chapter: always route submission through the event, so there is exactly one place where the decision to accept or reject the data is made, and no path around it.

Another way to trigger the submit event is the requestSubmit method, mentioned briefly before, which is useful when a form should be submitted after a step outside the form, such as confirming a choice in a dialog. Because requestSubmit fires the submit event, your validation still runs, unlike the older submit method, which bypasses it. Choosing requestSubmit for programmatic submission keeps the single-gate principle intact: every submission, however triggered, passes through the same handler and the same checks.

Check: Why listen for submit on the form rather than click on the button?

Section 21 — Preventing the default submit

Every browser event has a default action, and for a form's submit event, that action is to send the form to its action address and load the reply, which usually means leaving the current page. That behaviour predates JavaScript form handling, and it is still the right behaviour when a form has no script at all. But when your JavaScript is meant to check the fields first, the default action is premature: it would send unchecked data and navigate away before your validation had a chance to run. The method event.preventDefault() stops the default action for that event. Called at the start of your submit handler, it keeps the visitor on the page and hands control to your code, which can then validate, show errors, or send the data itself with fetch.

form.addEventListener("submit", (event) => {
  event.preventDefault();
});

Order matters here. Call preventDefault() first, before any validation or network request, so that no path through your handler can accidentally let the default submission slip through. A handler that validates first and only calls preventDefault() when it finds an error has a flaw: on a valid submission, the browser will still perform its default navigation, reloading the page and discarding the very success message your code was about to show. Preventing first and deciding afterwards is the safe order, and it also makes the handler easier to read, because the first line announces that this form is handled by JavaScript. If you later decide a form should submit normally after all, removing that one line restores the default behaviour, which is a clean, reversible decision.

There is a common confusion between preventDefault() and stopping the event entirely. preventDefault() stops the browser's default action, but it does not stop other handlers listening for the same event, and it does not stop the event travelling to other elements. That is usually what you want: your validation handler prevents submission, while any other listener, such as analytics, can still run. The method that stops other handlers is stopPropagation(), and it is rarely needed for forms. Another point worth noting is that preventDefault() only works if the handler is attached to an event that can be prevented. The submit event can be. A click on a plain button, which has no default submission, has nothing to prevent. So the pattern is specific: listen for submit on the form, prevent the default first, and then do your work. It is a small method with a large effect, and it is the hinge on which all client-side form handling turns.

A common debugging symptom of a missing preventDefault is a page that flashes and reloads on submit, with any success message disappearing instantly. If you see that flash, check the first line of your submit handler, because the browser's default navigation is the most likely cause. Adding preventDefault at the top, and then re-testing, will usually reveal the success message that was previously hidden by the reload, confirming that your handler was working all along and only the navigation was hiding its result.

Check: What would happen on submit if you forgot preventDefault?

Section 22 — FormData in one go

Reading fields one by one works well for a small form, but a form with ten or twenty fields becomes tedious and error-prone that way: each field needs selecting, reading, and naming, and forgetting one is easy. FormData solves that by gathering a whole form at once. The expression new FormData(form) builds an object containing a name-and-value pair for every named, enabled field in the form, in a single step. Text fields contribute their text, selects contribute the chosen option's value, ticked checkboxes contribute their values, and radio groups contribute the checked button's value. Unticked checkboxes and disabled fields contribute nothing, matching the rules of an ordinary submission.

Once you have a FormData object, you can read individual values with .get(name), which returns the first value for that name, or .getAll(name) for all values sharing a name, such as a group of checkboxes. You can loop over every entry with a for...of loop over data.entries(), or check whether a field exists with .has(name). For many tasks, reading a few values with .get() is all you need, and the code stays short and readable. The example below shows the simplest use: build the FormData, then read the email value from it, without selecting the email field at all.

const data = new FormData(form);
const email = data.get("email");

FormData has a second life that becomes important in Chapter 22. It can be sent directly as the body of a fetch request, and the browser will encode it in the same format as an ordinary form submission. That makes it a bridge between the old way of submitting forms and the new way: your validation can use FormData to read values, and your sending code can use the same object to transmit them. There are limits to know. FormData values are strings, or File objects for file inputs, so numbers still arrive as strings and need converting. And FormData only includes fields that have a name, which returns to the lesson of Section 6: a nameless field is invisible to FormData, just as it is invisible to an ordinary submission. Used with named fields and a submit handler, FormData turns the chore of reading a large form into one line, and leaves your code free to concentrate on checking and using the values.

FormData also handles file inputs gracefully, giving you File objects for chosen files, which can then be sent with fetch as multipart data. That makes it a natural bridge not only for text forms, but for upload forms too, where reading each file field by hand would be more work. For text-only forms, its main benefit remains simplicity: one object, built from the form, holding every named value, ready to be read with get or sent as a body, without a separate selection for each field.

Check: What does new FormData(form) collect, in one sentence?

Section 23 — Getting values by name

Inside a form, the elements collection gives you access to its fields by name or by position. The expression form.elements.email reaches the field named email, and form.elements["full-name"] reaches a field whose name contains a hyphen, where dot notation would not work. This approach keeps your code readable, because it uses the same names the form itself uses, and it keeps the HTML and the JavaScript in agreement: rename a field in the HTML and you must rename it in the code, which is a visible, searchable change rather than a silent mismatch. It also survives changes to ids, because it does not depend on them at all. A field can be restyled, relabelled, and given a new id for layout purposes, and code that reaches it by name keeps working.

The elements collection also handles groups gracefully. For a radio group, form.elements.size returns a collection of the group's buttons rather than a single field, and you can find the checked one among them or read its value through the collection. For a single field, it returns the field itself. That difference is worth testing when you first use it, because code that assumes a single field will behave unexpectedly when handed a group. A robust pattern is to use a selector for the checked radio button when you need a group's value, and form.elements for single fields, each tool for the job it fits. Another useful property is that form.elements only contains fields belonging to that form, so two forms on one page with similar field names do not interfere with each other. Code that selects by id across the whole document can accidentally grab a field from the wrong form; code that goes through a specific form's elements cannot.

There is a broader design point here. Reaching fields through their form, rather than through the whole document, expresses an idea: these fields belong to this task. It makes the code's structure mirror the page's structure, and it makes each form handler self-contained, able to be copied to another page with its form and still work. When a form grows, or when a page gains a second form, that self-containment pays for itself many times over. For small scripts, selecting by id is perfectly fine, and you will see both styles in real code. But knowing form.elements gives you a tool that scales, and it deepens your understanding of how a form organises its fields. The form is not just a container; it is an object with a named collection of its parts, ready to be used.

A related convenience is that form.elements also gives you the form's length, the number of fields it contains, and it lets you loop over all fields, for example to clear all errors or to check all fields in a submit handler. Looping over elements, rather than listing each field by hand, makes a handler that works for any number of fields, and it stays correct when fields are added or removed, because the loop adapts to the form's current contents. That adaptability is a small benefit that grows as a form grows.

Check: How would you reach the field named "message" through form.elements?

Section 24 — Listening to input events

The input event fires every time a field's value changes. Every keystroke, every paste, every cut, every deletion, and every choice in a dropdown or checkbox triggers it. That immediacy makes it the event for live feedback: a character count that updates as someone types, a password strength hint that strengthens as the password grows, a live preview of a message, or a search that updates as the visitor types, though Chapter 22 will show why searches usually need debouncing too. The handler for an input event runs while the visitor is still in the middle of their thought, so it can guide them before they finish, rather than correcting them afterwards. That guidance is powerful when used kindly and annoying when used carelessly, a balance Section 37 examines.

nameInput.addEventListener("input", () => {
  count.textContent = nameInput.value.length;
});

Because the input event fires so often, its handler must be light and fast. A handler that does a small update, such as setting a count or toggling a class, is fine on every keystroke. A handler that performs a network request, a heavy calculation, or a full re-render of a list on every keystroke will make typing feel sluggish, because the browser must run that work between keystrokes. Keep input handlers to cheap, local work, and move expensive work to debounced handlers, blur handlers, or submit handlers. There is also a correctness point: an input handler sees intermediate states. A half-typed email address is not yet a valid email, and a half-typed number is not yet in range, so an input handler that shows errors for intermediate states will scold the visitor for typing. The usual solution is to use input events for neutral feedback, such as counts and hints, and to reserve error messages for blur or submit, or to show errors on input only after the field has already been flagged once. That rule keeps live feedback helpful without turning typing into an argument with the page.

Finally, note that the input event works on text fields, textareas, selects, checkboxes, and radio buttons, though for checkboxes and radio buttons the change event is more commonly used. It does not fire for programmatic changes: setting a field's value in code does not trigger an input event, which means your handler will not run when you pre-fill a field. If you need the handler to run after a programmatic change, you must call it yourself or dispatch the event manually. Knowing that keeps you from puzzling over a count that updates when the visitor types but not when your code fills the field.

An input handler that updates a preview, such as a live preview of a message, should also consider performance for long texts. Updating a preview on every keystroke for a short message is fine, but for a very long document, re-rendering the whole preview on each keystroke could lag. In that case, debouncing the preview update, waiting for a pause as in Chapter 22, keeps typing smooth while still giving a live preview. The input event tells you when to consider updating; it does not oblige you to update instantly on every event.

Check: When exactly does the input event fire?

Section 25 — Listening to change events

The change event fires when a field's value is finished changing, and the meaning of "finished" depends on the field type. For a text field or textarea, change fires when the field loses focus after its value was edited: the visitor types, then tabs or clicks away, and only then does change fire. For a checkbox, a radio button, or a dropdown, change fires immediately when the choice is made, because there is no half-finished state to wait for; ticking a box is a complete act. This difference makes change a good moment for checks that should run once per field rather than once per keystroke. A field's value can be validated when the visitor moves on from it, a summary can be updated when an option is chosen, and a dependent section can be shown or hidden when a checkbox is ticked, all without the noise of firing on every keystroke.

Change and input are complementary, not competing, and choosing between them is a design decision about timing. Input says "the value just changed, right now". Change says "the visitor has settled on a value". For a live character count, input is right, because the count should track every keystroke. For a validation message on a text field, change is often kinder, because it waits until the visitor has finished typing before judging the result. For a checkbox that reveals extra fields, change is right, because the decision is made the moment the box is ticked. Many forms use both: input for live hints, change for validation and side effects, and submit for the final check. Using them together, each for its proper moment, gives a form that feels responsive without feeling impatient.

There is one more distinction worth knowing. Like input, change does not fire for programmatic changes; setting a value in code does not trigger it. And change fires only if the value actually changed. A visitor who focuses a text field, types nothing, and leaves will not trigger a change event, because nothing changed, though a blur event will still fire. That makes change unsuitable for detecting that a visitor merely visited a field; for that, blur itself is the event. When you combine these events thoughtfully, input for immediacy, change for settled values, blur for visits, and submit for the final gate, you have a complete vocabulary for responding to a form at exactly the right moments, and the rest of this chapter is largely about choosing the right moment for each kind of check.

Change events are also the natural trigger for dependent fields, such as a section that appears when a checkbox is ticked, or a second dropdown whose options depend on the first. Because change fires when a choice is settled, the dependent section appears at the right moment, when the visitor has made their decision, not while they are still browsing options. Using change for those side effects keeps the page's behaviour aligned with the visitor's decisions, rather than with intermediate states they have already moved past.

Check: How does change differ from input for a text field?

Section 26 — Trimming whitespace

Visitors rarely type with perfect precision. They leave a space at the start of a name after tapping the field, they paste an email address copied from another app with a trailing space attached, or they accidentally press the space bar in an otherwise empty field. To the eye, a field containing a single space looks empty, but to code that checks only whether the value is an empty string, it is not empty at all: it is a string containing one space, and a naive check would pass it as filled in. Trimming solves this. The string method .trim() returns a copy of a string with whitespace removed from both ends, leaving the middle untouched. A name typed as " Ada " becomes "Ada", and a field containing only spaces becomes an empty string, which your empty check can then catch correctly.

const name = nameInput.value.trim();

Trim before you validate, not after, because validation is a judgement about the meaningful content, and leading or trailing spaces are not meaningful content in a name, an email address, or a username. If you validate first and trim later, you may accept a spaces-only field and then store an empty value, or reject a value that trimming would have fixed. There is also a storage question: when you send or save a trimmed value, you are storing what the visitor meant, not the accidental spaces around it. That keeps databases clean and avoids mismatches later, such as a login failing because the stored email has a trailing space the visitor will not type next time. Trimming is a small, cheap operation with an outsized effect on data quality, and it should be a reflex for text fields.

There are two exceptions where trimming is wrong. Passwords should not be trimmed, because a space at the start or end of a password might be part of the password the visitor deliberately chose, and trimming it would change their password without telling them. Section 31 returns to this point for password confirmation. The other exception is any field where leading or trailing spaces are meaningful, such as a code snippet or a formatted piece of text in a textarea, where the visitor's spacing is part of the content. For ordinary names, emails, usernames, and titles, though, trimming is correct. A related method, .trimStart(), removes whitespace only from the start, and .trimEnd() only from the end, but plain .trim() covers the common case. Make it the first step of reading a text value, and your empty checks, length checks, and comparisons will all judge the content the visitor intended, not the accidents around it.

Trimming is also useful when comparing values, such as checking whether a username is already taken, or whether two email addresses match. An email with a trailing space, pasted from another app, should match the same email without the space, because the space is not part of the address. Trimming both values before comparing them ensures that accidental whitespace does not cause a false mismatch, and it is a small step that prevents a confusing 'these do not match' message for values that a human would consider identical.

Check: Why trim a value before checking whether it is empty?

Section 27 — Checking for empty fields

The simplest and most important validation is asking whether a required field has anything in it. After trimming, the test is straightforward: if the trimmed value is the empty string "", nothing meaningful was entered. That test applies to text inputs, textareas, and email fields alike, and it should be the first check in your validation, because there is no point checking the shape of a value that does not exist. A missing email address is not a badly shaped email address; it is a missing answer, and the message should say so. Distinguishing "you left this blank" from "what you typed does not look right" helps the visitor fix the right problem, and it helps your code stay organised, with one check per kind of problem.

if (nameInput.value.trim() === "") {
  showError(nameInput, "Please enter your name.");
}

The error message matters as much as the check. A vague message such as "Invalid input" or "Something is wrong" forces the visitor to guess which field failed and why. A good message names the field and states what is needed: "Please enter your name", "Please write a message", or "Please choose a ticket type". It should be phrased as guidance, not blame; the visitor has not committed an offence, they have simply missed a step, and a helpful tone keeps them willing to finish. Placing the message beside the field, as Section 34 explains, completes the picture: the visitor sees the field, the message, and the fix in one glance, without hunting.

Empty checks also need care for field types that are not text. A select with a prompt option whose value is empty is effectively unchosen, so checking that its value is not empty catches the unchosen case. A radio group with no button checked has no value at all, so checking for the absence of a checked button is the equivalent test. A checkbox that must be ticked, such as a terms agreement, is checked through its .checked state rather than its value. In each case the underlying question is the same: did the visitor provide the required answer? Framing validation as a set of such questions, one per field, keeps the code clear and the messages specific. Start every form's validation with the empty checks, because they are the most common failure, the easiest to fix, and the most frustrating to encounter as a vague error at the end of a long form.

An empty check should also consider fields that contain only a placeholder-like value, such as a select whose prompt option is still selected, or a text field containing the word 'Enter your name' because a visitor typed over a hint incorrectly. Those cases are not empty strings, but they are effectively unanswered, and your validation should treat them as such, either by giving the prompt an empty value, as Section 13 recommends, or by checking for known placeholder text. An empty check that only tests for an empty string will miss those effectively empty answers.

Check: After trimming, what value tells you a field is empty?

Section 28 — Checking length

Some fields need a minimum length, a maximum length, or both. A username might need at least 3 characters to be distinctive, a password might need at least 8 characters to be reasonably strong, and a short biography might be capped at 200 characters so it fits its display. JavaScript gives you a string's length with the .length property, which counts characters, and the check itself is a simple comparison. The example pattern is to test the trimmed value's length against the limit and, if it falls short or overshoots, to show a message that states the limit. Stating the limit in the message is essential: "Username must be at least 3 characters" tells the visitor exactly what to do, while "Username is too short" leaves them guessing whether the minimum is 2, 3, or 10.

Length checks interact with other checks in ways worth thinking through. A minimum length check on an empty field will also fail, since an empty string has length zero, so it is cleaner to check emptiness first and give the "please enter" message, then check length and give the length message. That way each failure gets the message that matches it, instead of a visitor who left a field blank being told their input is too short. A maximum length check raises a different design question: should the visitor be stopped from typing beyond the limit, or allowed to type and then told to shorten it? The HTML maxlength attribute stops typing at the limit, which is simple but can confuse a visitor whose keystrokes suddenly stop working. A live character count, showing how many characters remain, is usually kinder, because it warns before the limit is reached. Many forms combine both: a count for guidance, and a validation check as the final guard.

Counting characters also has a subtlety. JavaScript's .length counts code units, not always what a person would call characters, and some emoji or accented characters count as two units. For ordinary usernames and messages this rarely matters, but for a strict limit on a field that might contain emoji, the count can surprise you. For this book's purposes, .length is the right tool, and the subtlety is noted here so it does not ambush you later. The broader lesson of length checks is that a limit only helps if the visitor knows it before they break it. Put the limit in the label, the hint, or the live count, and the validation message becomes a reminder rather than a surprise.

Length checks also apply to codes and identifiers where an exact length is required, not just a minimum or maximum. A code that must be exactly six characters, for example, fails if it is five or seven, and the message should say so: 'Code must be exactly 6 characters'. An exact-length check is simply a minimum and a maximum with the same value, but stating it as an exact requirement in the message is clearer than stating two separate limits, because the visitor immediately knows the target, not just a range to guess within.

Check: How do you find how many characters are in a string in JavaScript?

Section 29 — Checking an email shape

An email address has a recognisable shape: some text, an @ sign, more text, a dot, and more text after the dot, as in "ada@example.com". A basic email check tests for that shape. It catches the most common typos: a missing @ sign, a missing dot, or a completely empty-looking entry such as "hello". The browser's built-in email check does this for you when you use type="email", and your own check can do the same with a simple pattern or with a few string tests. Either way, the purpose is the same: to catch obvious mistakes early, while the visitor is still there to fix them, rather than discovering later that a confirmation message went nowhere.

What a shape check cannot do is equally important, and it is a limit that beginners often miss. A shape check cannot tell whether the address belongs to anyone, whether the mailbox exists, or whether the visitor typed their own address. The string "nobody@nowhere.example" has a perfect email shape and belongs to nobody. A visitor can also mistype their own address in a way that still has a valid shape, such as "ada@gmial.com" instead of "ada@gmail.com", and no shape check will catch that, because both shapes are valid. The only way to prove an address is real and belongs to the visitor is to send a message to it and ask the visitor to confirm, usually by clicking a link or entering a code. That confirmation step belongs to the server side of an application, beyond this chapter, but knowing it exists keeps your expectations honest.

Because of that limit, email validation should be kept proportionate. A short, forgiving check that catches missing @ signs and missing dots is good. A long, strict pattern that tries to encode every rule of email addresses is bad, because email addresses have surprising valid forms, and a strict pattern will reject real addresses while still not proving anything about the ones it accepts. A good rule is to let the browser's built-in check do the first pass, add your own gentle check if you need a clearer message, and reserve real proof for the confirmation step. Also consider asking for the address once, carefully, rather than twice: asking twice doubles the chance of a typo being copied, and a confirmation email catches mistakes better than a second field does. Shape checks are a net for obvious errors, not a proof of identity, and designing with that understanding keeps forms both usable and honest.

A helpful companion to a shape check is to suggest a correction for a common typo in a domain, such as 'gmial.com' where 'gmail.com' was likely intended. Some forms offer a gentle suggestion, 'Did you mean gmail.com?', without forcing the visitor to accept it, which catches a frequent mistake that a shape check cannot, because the mistyped domain still has a valid shape. That suggestion is a kindness, not a validation rule, and it should never block submission, because the visitor may genuinely use an unusual domain that only looks like a typo.

Check: Why can a shape check never prove an email address is real?

Section 30 — Regular expressions in plain words

A regular expression, usually shortened to regex, is a small pattern written in a compact notation that describes what a string should look like. Instead of writing code that checks each character one by one, you write a pattern, and JavaScript tests whether a string matches it. For example, the pattern for a four-digit code might say "exactly four characters, each a digit from 0 to 9". The pattern for a simple email shape might say "some letters, then an @ sign, then some letters, then a dot, then some letters". Regex is powerful because one short pattern can describe a whole family of acceptable strings, and it is used far beyond forms, in searching text, validating data, and processing files.

That power comes with a cost: regex notation is dense and easy to get wrong. A single misplaced character can change what a pattern accepts, sometimes in ways that are hard to spot by reading it. A pattern meant to accept only digits might accidentally accept letters if a range is written wrongly, and a pattern meant to require a dot might accept any character if the dot is not escaped, since in regex an unescaped dot means "any character". These are not exotic mistakes; they are the usual ones, and they are why regex should be used sparingly in forms. For most fields, the built-in checks of the input type, a length check, and a simple comparison are clearer and safer. Regex earns its place when the required shape is specific and not covered by a built-in type, such as a membership code, a postal code format, or an identifier with a fixed pattern.

When you do use regex, test it deliberately. Try values that should pass, values that should fail, and edge cases such as empty strings, values that are almost right, and values with unexpected characters. JavaScript's .test() method returns true or false for a match, and online regex testers can show you what a pattern matches, though you should test with your own examples too, not only with the tester's. Keep the pattern short, add a comment explaining in plain words what it requires, and always explain the required format to the visitor in the label or hint, because a regex can only say whether a value matches; it cannot explain itself to a human. Used with that discipline, regex is a sharp tool. Used casually, it is a source of mysterious rejections that frustrate visitors and baffle the developer who wrote it.

When you test a regex, include the empty string as a test case, because many patterns accidentally match an empty string, or fail to match it when they should. An empty value should usually be handled by the required check, not by the pattern, and a pattern that matches empty will let an optional field pass with a value that is technically empty but was never intended to be checked. Testing empty, too short, too long, and just right, for every pattern, is a small discipline that catches most pattern errors before they reach a visitor.

Check: What is a regular expression, in your own words?

Section 31 — Matching two password fields

Many sign-up forms ask for a password twice: once in a password field and once in a confirmation field. The purpose is to catch typos, because a password is hidden as it is typed, and a single mistyped character would lock the visitor out of their new account. Validation for this pair is conceptually simple: the two values must be exactly equal, character for character. If they match, the visitor almost certainly typed what they intended, twice. If they differ, even by one character, something is wrong, and the form should say so plainly and let the visitor retype both. The message should name the problem, "Passwords do not match", rather than a vague "Invalid password", so the visitor knows the issue is the mismatch, not the password's strength or length.

There is one important rule for this comparison: do not trim passwords. Section 26 recommends trimming for names and email addresses, because spaces around those are almost certainly accidents. Passwords are different. A space at the start or end of a password might be a deliberate part of it, chosen by a visitor who likes a passphrase with spaces, or it might be an accident, but your code cannot tell which. Trimming would silently change the password the visitor intended, and they would later fail to log in with the password they thought they set. The safe approach is to compare the two fields exactly as typed, without trimming either, and to let the visitor decide what their password is. If they mistyped a space in one field, the mismatch message will prompt them to retype both, which is the correct outcome.

A design question follows: should the confirmation field still be required at all? Some modern forms drop it, arguing that a "show password" toggle, discussed in Section 8, lets the visitor verify what they typed more directly, and that a second field doubles the typing without adding much safety for visitors who use password managers. Others keep it, arguing that it catches typos for visitors typing by hand. Both positions are defensible, and the choice depends on your audience. What is not defensible is a confirmation check that is vague, that trims, or that echoes either password back to the page. Compare exactly, report clearly, and never display the values. A password confirmation is a small check with a single job, and doing it exactly, without extra cleverness, is what makes it trustworthy.

A further kindness for password confirmation is to allow the visitor to see both passwords, with a show toggle, before submitting, so they can compare them visually themselves. A visitor who can see that both fields contain the same text is less likely to be surprised by a mismatch message, and a visitor who spots a difference can fix it before submitting, rather than after. The toggle does not replace the code's comparison, but it gives the visitor the same information the code has, which is fair and helpful.

Check: Should you trim a password before comparing the two fields? Why?

Section 32 — Checking numbers in range

A number field's value arrives in JavaScript as a string, so the first step of checking it is conversion. The Number() function turns a string into a number: Number("42") gives 42, and Number("3.5") gives 3.5. But not every string is a number. Number("abc") gives a special value called NaN, short for "not a number", which is JavaScript's way of saying the conversion failed. NaN has an unusual property: it is not equal to anything, not even itself, so testing value === NaN will always be false, even when the value is NaN. The correct test is Number.isNaN(value), which returns true only for NaN. A robust number check therefore has two parts: convert, then test for NaN before doing anything else, because every comparison with NaN is false, and a check written as if (age < 18 || age > 120) would silently pass a NaN value, since both comparisons are false.

const age = Number(ageInput.value);
if (Number.isNaN(age) || age < 18 || age > 120) {
  showError(ageInput, "Enter an age between 18 and 120.");
}

Once you know the value is a real number, the range check itself is simple: test whether it is below the minimum or above the maximum, and show a message stating the allowed range. Stating the range matters, because a message such as "Invalid age" leaves the visitor guessing, while "Enter an age between 18 and 120" tells them exactly what to fix. Also consider whether whole numbers are required. A quantity of tickets should be a whole number, and a value such as 2.5 tickets makes no sense. The test Number.isInteger(value) returns true only for whole numbers, and it is the right companion to a range check when fractions are meaningless. Similarly, consider whether the range's endpoints are included. "Between 18 and 120" usually includes both 18 and 120, and your comparison operators should reflect that: use < for below the minimum and > for above the maximum, so the endpoints themselves pass. Getting the endpoints wrong is a classic off-by-one error, and the fix is to test the boundary values deliberately: try 17, 18, 120, and 121, and confirm that only the middle two pass.

For a quantity field, also consider whether the value should be a whole number, and whether zero is allowed, before writing the range check. A quantity of two and a half tickets is meaningless, so an integer check belongs alongside the range check, and a quantity of zero may mean the visitor wants nothing, which could be valid for an optional extra but not for a main order. Writing those rules down, as part of the check, turns a vague 'enter a number' into a precise set of expectations that the visitor can meet.

Range checks should also be tested at their boundaries, because off-by-one errors are among the most common validation mistakes. Trying a value just below the minimum, exactly at the minimum, exactly at the maximum, and just above the maximum, and confirming that only the middle two pass, will catch a comparison written with the wrong operator before a visitor does. Boundary testing takes a minute and prevents a whole class of subtle rejections.

Check: What does Number.isNaN() test for?

Section 33 — Checking a date is sensible

A date input returns a string in year-month-day order, which is a gift for validation, because it removes format ambiguity. But a correctly formatted date can still be nonsensical for its purpose. A booking date in the past, an end date before a start date, or a date of birth in the future are all well-formed dates that fail the logic of the task. Checking a date is therefore a two-stage job: first confirm it is a real date, then confirm it makes sense in context. To work with dates, turn the string into a Date object with new Date(value). A Date object represents a point in time, and two Date objects can be compared with the usual operators: if end < start, the end comes before the start, which is the condition to catch for a booking or an event.

Comparing dates has traps worth knowing. A Date object includes a time of day, and a date input's string is interpreted as midnight at the start of that day. Comparing a booking date with "today" therefore needs care: a booking for today, interpreted as midnight this morning, is earlier than the current moment, even though today is not in the past. The usual fix is to compare dates at day precision, by clearing the time part of both dates to midnight before comparing, or by comparing the year-month-day strings directly, which sort correctly as text. Either approach works; what matters is deciding what "today" means for your task and testing the boundary. Test yesterday, today, and tomorrow against a "not in the past" rule, and confirm that today passes if that is your intention.

Also consider the relationship between two dates. A common pattern is a start date and an end date, where the rule is that the end must not be before the start, and often must be after it. Validate both fields individually first, so a missing or malformed date gets its own message, then validate the pair together, showing the error on the end date field, since that is the one that must move. The message should state the rule: "End date must be after the start date" is clear, while "Invalid date" is not. As with numbers, state the rule, test the boundaries, and remember that a date input's min and max attributes can prevent many bad dates at the picker stage, but your JavaScript check remains the final guard for typed dates and for rules that involve two fields together.

For a date of birth field, the sensible range is also worth stating: a birth date should not be in the future, and it should not be implausibly far in the past, such as more than 120 years ago. Those bounds are not about format, but about plausibility, and they catch typos such as a birth year typed as 2026 instead of 1926, which is a well-formed date but an implausible birth date for most visitors. Plausibility checks, stated in the message, turn a format check into a meaningful check for the task at hand.

Check: How would you check that an end date is not before a start date?

Section 34 — Showing errors next to the field

Where an error message appears matters almost as much as what it says. The most useful place is right beside the field it belongs to, usually directly underneath it, where the visitor's eyes already are when they are fixing that field. A visitor correcting a form should be able to see, in one glance, the field, what they typed, and what is wrong with it. That glance is only possible when the message sits with the field. The alternative, a single pile of messages at the top of the form, forces the visitor to read a list, remember which message belongs to which field, scroll to the field, and fix it, all while holding the mapping in their head. For a long form, that mapping is a real cognitive burden, and it is a common reason visitors abandon a form that is nearly complete.

Implementing beside-the-field errors is straightforward. For each field, reserve a small element under it, such as a paragraph with a class for styling, and an id that the field references with aria-describedby, as Section 48 explains. Your error function writes the message into that element and perhaps adds a class to the field for a red border; your clear function empties the element and removes the class. Keeping one message element per field, rather than creating and destroying elements on the fly, makes the code simpler and the layout more stable, because the space for the message can be reserved even when it is empty, preventing the page from jumping as messages appear and disappear. A layout jump is a small annoyance that feels surprisingly unprofessional, and reserving space is an easy way to avoid it.

There is still a place for a summary at the top of the form, but as a supplement, not a replacement. On submit, if several fields fail, a short summary listing the problems, with links that jump to each field, helps a visitor who wants an overview before fixing things one by one. The summary should never be the only place errors appear, because a visitor fixing fields one at a time will not return to the top to re-read it. The rule to carry forward is: the field owns its error, and any summary points back to the field. Colour helps too, but it must not be the only signal: a red border plus a written message works for everyone, while a red border alone means nothing to a visitor who cannot distinguish the colour. Message, placement, and a non-colour signal together make an error that any visitor can find, understand, and fix.

When a field is inside a fieldset or a long section, the error's position should still be beside that field, not at the section's top, because the visitor's attention is on the field they are fixing. A section-level error, even for a field in that section, forces the same hunting that a form-level pile does, on a smaller scale. The principle scales down as well as up: the error belongs with the field, wherever that field sits, and any summary elsewhere is a pointer back to it, never a replacement for it.

Check: Why is an error message more useful beside its field than at the top of the form?

Section 35 — Clearing errors when the user fixes them

An error message has a lifespan: it should appear when a problem is found, and disappear when the problem is fixed. A message that lingers after the fix is worse than unhelpful; it is actively misleading. It tells the visitor that something is still wrong when it is not, it undermines trust in the form's other messages, and it can cause a visitor to "fix" a field that was already correct, breaking it again. The fix is to clear a field's error as soon as the field changes. Listening for the input event on each field, as in Section 24, and clearing that field's message in the handler, keeps the page's state honest: the message reflects the field's current content, not the content it had when the error was first shown.

Clearing on input is not the same as re-validating on input, and the distinction matters. Re-validating on every keystroke would show new errors for half-typed values, which Section 37 warns against. Clearing, by contrast, simply removes the old message and waits. The field is not declared valid; it is merely no longer flagged with a stale complaint. The next judgement comes at blur or at submit, when the value is settled. A refined version of this pattern is to re-validate on input only after a field has already shown an error once. Before the first error, the visitor types undisturbed. After an error, each keystroke re-checks the field, so the message disappears the moment the value becomes valid, giving satisfying, immediate feedback that the fix worked. This "validate on input after the first error" pattern is used by many well-designed forms, because it balances patience with responsiveness.

Implementing clearing is simple if each field has its own message element, as Section 34 recommends. The input handler for a field empties its message element and removes any error styling from the field. If you use the Constraint Validation API, covered in Section 45, you should also clear any custom validity message with setCustomValidity("") when the field changes, or the field will remain invalid in the browser's eyes even though your visible message has gone. The principle behind all of this is that a form is a live conversation, not a printed report. Messages should track the present, not the past, and a form that updates its feedback as the visitor works feels alive, attentive, and trustworthy, while a form that leaves old complaints standing feels careless, however correct its initial checks were.

Clearing should also remove any error styling from the field, such as a red border, at the same moment the message is cleared, because a red border without a message is a signal without an explanation, and a message without a border is an explanation without a visible marker. Clearing both together keeps the field's appearance and its message in agreement, so the visitor is never left with a field that looks wrong but says nothing, or says something wrong but looks fine. That agreement is a small detail that makes a form feel coherent.

Check: When should an error message disappear?

Section 36 — Validating on blur

Blur is the moment a field loses focus. It happens when a visitor finishes with a field and moves on, by pressing Tab to go to the next field, by clicking elsewhere on the page, or by any other means of leaving the field. That moment is a natural checkpoint for validation, because the visitor has finished their answer to that field, at least for now, and is ready to hear whether it is acceptable. Checking on blur catches mistakes early, while the field is still fresh in the visitor's mind and before they have invested effort in the rest of the form. It is earlier than the submit check, which waits until the end, and later than the input check, which judges while typing is still in progress. That middle position is what makes blur validation friendly.

To validate on blur, listen for the blur event on a field, and run that field's check in the handler. If the check fails, show the field's error beside it; if it passes, clear any old error. The handler checks only that one field, not the whole form, which keeps it fast and keeps the feedback focused. A visitor tabbing through a form will therefore get feedback field by field, fixing small problems as they go, rather than facing a list of errors at submit time. That steady, incremental correction is less overwhelming than a wall of errors at the end, and it means the submit check often finds nothing left to complain about, which makes submission feel smooth.

Blur validation has limits to respect. A visitor who never visits a field will never trigger its blur event, so a field left untouched will have no blur error. That is why the submit check remains essential, as Section 38 insists: blur is an early warning, not a replacement for the final gate. Blur can also fire when a visitor merely passes through a field, focusing it and leaving without typing, which would show a "please enter" error for a field they had not yet intended to complete. Some forms soften this by only validating on blur if the field has been changed, or by waiting until the visitor has visited the field and then left it with a value. Those refinements are worth considering once the basic pattern works. The core idea, though, is simple: blur marks the end of a field's turn, and it is a kind and timely moment to check that field's answer.

Blur validation should also be skipped for a field the visitor has not changed, to avoid showing an error for a field they merely tabbed through on their way elsewhere. A field that was focused and left untouched has not been answered, and a 'please enter' error for it, shown before the visitor has reached that part of the form, can feel premature. Checking, in the blur handler, whether the value has changed since focus, and only validating when it has, keeps blur feedback focused on answers the visitor actually gave.

Check: What is blur, and why is it a good moment to validate a field?

Section 37 — Validating as you type, with care

Live validation, checking a field on every keystroke through the input event, offers the fastest possible feedback. A password strength meter that fills as the password grows, a username availability hint, or a character count that ticks down are all live feedback, and when done well they make a form feel responsive and alive. The visitor does not have to wait until the end to learn something; the page responds as they work. That responsiveness is the attraction of validating as you type, and it is why many modern forms use it. But it has a trap that is easy to fall into: while a visitor is typing, their value is necessarily incomplete, and an incomplete value will often fail a check that the finished value would pass.

Consider an email address. After the first few keystrokes, the visitor has typed "ada", which has no @ sign and no domain. A live check that shows "Please enter a valid email address" at that moment is technically correct and practically absurd: the visitor is in the middle of typing, and the error scolds them for not having finished. Repeated on every field, that scolding makes a form feel hostile, and visitors learn to ignore its messages, which defeats the purpose of validation entirely. The same applies to a minimum length check on a username, a pattern check on a code, or any check whose passing value takes several keystrokes to build. Judging a half-built answer by the standard of a finished answer is unfair, and visitors rightly resent it.

The solution is not to abandon live feedback, but to time it kindly. A widely used rule is to show live feedback only after a field has already shown an error once. Before the first error, the field is judged at blur or at submit, giving the visitor peace to type. After an error, each keystroke re-checks the field, so the visitor gets immediate confirmation when their fix works, which is satisfying rather than scolding, because they are now deliberately correcting a known problem. Another kind rule is to use live feedback for neutral information, such as character counts and strength hints, which inform without judging, while reserving error messages for blur and submit. A strength meter that says "Good" as a password grows is encouragement; an error that says "Password too short" after three characters is a complaint. The difference is not in the code, which is similar in both cases, but in the tone and timing of the feedback. Live validation done with care feels like help; done without care, it feels like surveillance.

A live strength hint for a password is a good example of neutral live feedback, because it describes the password's quality without declaring it invalid. A meter that moves from weak to strong as the password grows encourages the visitor to improve it, while an error that appears after three characters discourages them. The difference is not in the timing, but in the framing: a hint informs, an error judges, and live feedback should inform until the value is settled, when judgement becomes appropriate.

Check: Why can validating on every keystroke annoy a visitor typing an email address?

Section 38 — Validating on submit as the safety net

No matter how much live checking, blur checking, and built-in browser validation a form has, the submit check remains indispensable, because it is the only check that is guaranteed to run on every field, in every case, before the data is used. Live checks only run on fields the visitor typed in. Blur checks only run on fields the visitor visited. Built-in checks can be bypassed by a browser that does not support them, or by a form with novalidate. Autofill can fill a field without firing the events your live checks listen for. A visitor can even submit a form with JavaScript disabled, in which case none of your scripted checks run at all. The submit handler is the single point through which every submission must pass, and placing a complete validation there ensures that no path, however unusual, delivers unchecked data.

A submit check should validate every field, not only the ones that changed. It should run the same checks as the individual field checks, ideally by calling the same validation functions, so the rules are defined once and used everywhere. If a field fails, the handler shows its error beside the field, collects all the failures rather than stopping at the first one, so the visitor can fix everything in one pass, and then stops: it does not send or use the data. Only when every field passes does the handler proceed. That all-or-nothing gate is what makes the submit check a safety net rather than a suggestion. It also gives the visitor a clear contract: pressing the submit button either succeeds, with a success message, or explains exactly what still needs fixing. There is no third outcome in which the form silently accepts bad data.

There is a second reason the submit check matters, beyond catching missed fields. It is the moment when checks involving several fields together can run, such as confirming that two passwords match, that an end date follows a start date, or that a set of choices is consistent. Those cross-field checks cannot run at blur time for a single field, because they depend on more than one field's value, and they cannot run reliably on input, because the related field may not be filled yet. Submit time, when all values are present, is their natural home. So the submit handler is both the last gate and the only place where the whole form can be judged as a whole. Build it to check everything, every time, and let the earlier checks be what they are: helpful early warnings that make the final gate less surprising, not replacements for it.

The submit check is also the right place to focus the first failing field, so a keyboard visitor is taken directly to the problem, rather than left at the submit button wondering what went wrong. Moving focus to the first error, combined with its message beside the field, gives every visitor, including those who cannot see the page, a clear starting point for fixing the form. That focus move turns a list of errors into a guided correction, starting at the first problem and working through the rest in order.

Check: Why is a submit-time check still needed even with live validation?

Section 39 — The required attribute

The required attribute is the simplest validation tool in HTML, and it costs a single word. Adding required to a field tells the browser that the field must not be empty when the form is submitted. The browser then enforces that rule itself: if the visitor tries to submit with a required field empty, the browser blocks the submission, focuses the empty field, and shows its own message, such as "Please fill in this field". That behaviour works even if your JavaScript fails to load, is blocked, or contains a bug, which makes required a safety net beneath your safety net. It also works before any of your code runs, so a visitor gets immediate feedback without waiting for a script.

<input name="name" required>

The required attribute adapts to field types in sensible ways. For a text field, it means the value must not be empty, though note that a value of only spaces counts as filled for the browser's built-in check, which is why trimming in your own validation still matters. For a select, it means an option with a non-empty value must be chosen, which is why a prompt option should have an empty value, as Section 13 explains. For a checkbox, it means the box must be ticked, which suits a terms agreement. For a radio group, putting required on one button in the group means one button in the group must be selected. For a file input, it means a file must be chosen. In each case the browser understands what "filled" means for that field type, and you do not have to teach it.

There are limits, and they are worth stating plainly. Required only checks emptiness, not quality: a required email field can still contain a badly shaped address, though the email type's own check catches that, and a required text field can contain a single character. Required also relies on the browser's messages, which vary in wording and styling between browsers and cannot be fully customised. For a polished form you will often add your own messages on top, using the Constraint Validation API from Section 43, while keeping required as the underlying rule. The attribute is not a complete validation strategy, but it is a free, robust first layer, and a form that uses it is already better than a form that relies entirely on JavaScript that might not run. Use it on every field that is genuinely mandatory, and leave it off fields that are truly optional, because marking an optional field as required is a quick way to block visitors who have nothing to enter there.

A required field should also be marked as required in its label, with the word 'required' or a clearly explained symbol, so the visitor knows before submitting that the field must be filled. Relying only on the browser's submit-time message to reveal that a field was required wastes the visitor's time, because they discover the requirement only after trying to submit. Stating requirements upfront, in the label, is a courtesy that prevents errors, rather than merely reporting them after they happen.

Check: What does the browser do if a required field is empty on submit?

Section 40 — Built-in types do some validation

Choosing the right input type buys you validation for free, because the browser knows what each type's values should look like and checks them at submission time. An email field rejects a value with no @ sign. A URL field rejects a value that does not look like a web address. A number field rejects letters and, with min and max set, values outside the range. A date field only accepts real calendar dates, so February 30 is rejected without any code from you. These checks run as part of the browser's built-in validation, before your submit handler, and they block submission with the browser's own messages if they fail. They are not a replacement for your own checks, but they catch the simplest mistakes at the earliest possible moment, with no code at all.

The value of built-in checks goes beyond catching errors. They also guide the visitor towards correct input in the first place. A date picker prevents format confusion, a numeric keyboard prevents letters, and an email keyboard puts the @ sign within reach. Prevention is better than correction, and input types prevent a whole class of mistakes by making the wrong input hard to enter. They also communicate meaning to other software: autofill, password managers, and assistive technology all use the type to understand what a field wants, as Sections 9 and 10 note. A form built from specific types is therefore not only more validated, but more usable and more accessible, than the same form built entirely from generic text inputs.

Built-in checks have boundaries you should respect. They check shape, not truth: a well-shaped email may belong to nobody, a number in range may still be wrong for the task, and a real date may still be in the past when it should be in the future. They also depend on the browser: older or unusual browsers may enforce them weakly, and a visitor can bypass them by disabling JavaScript-free submission paths or by crafting a request directly. That is why the later sections of this chapter add your own checks on top, and why server-side validation, beyond this book's front-end scope, remains essential for any real application. Think of built-in type checks as the first net in a series: they catch the obvious mistakes cheaply, your JavaScript checks catch the task-specific ones, and the server catches anything that slips past both. A form with all three nets is trustworthy; a form with only the first is hopeful.

Built-in checks also help visitors who use autofill, because a browser filling an email field will fill it with an email-shaped value, and a number field with a number, matching the type's expectations. A form built from generic text inputs gives autofill no hints, so it may fill fields incorrectly or not at all. Specific types therefore improve not only validation, but also the whole filling experience, for visitors who rely on the browser to complete familiar fields for them.

Check: Name two checks a browser does for free when you pick the right input type.

Section 41 — The pattern attribute

The pattern attribute holds a regular expression that a field's entire value must match before the browser will accept it. It is written on the field itself, and it turns a text input into a shape-checked field without any JavaScript. A membership code made of exactly four digits, for example, can be enforced with pattern="[0-9]{4}", where [0-9] means any digit and {4} means exactly four of them. A code with letters, or with three or five digits, will be blocked at submission, with the browser's message. Pattern is therefore the bridge between the generic text input and the specific shape checks that input types provide for emails and URLs: it lets you define a shape for data that has no dedicated input type, such as codes, identifiers, and formatted references.

Using pattern well requires two habits. The first is to keep the pattern simple. A pattern that tries to capture every nuance of a complex format will become unreadable, hard to test, and likely to reject valid values, as Section 30 warns for regex generally. A short pattern for a clear, fixed format is good; a long pattern attempting to validate something like a full postal address is not, because addresses vary too much to be captured in one pattern. The second habit is to explain the expected format to the visitor in plain words, in the label or in a hint beside the field, such as "Four digits, for example 4821". The browser's default message for a pattern failure is often cryptic, along the lines of "Please match the requested format", which tells the visitor nothing about what the format is. Your hint text is what turns a pattern from a trap into guidance.

Pattern interacts with other validation in predictable ways. It only applies when the field has a value; an empty optional field with a pattern passes, because there is nothing to match, while an empty required field fails on the required check, not the pattern. Pattern is also checked by the browser before your submit handler runs, unless the form has novalidate, so your handler will normally only see values that already match the pattern, unless you have deliberately taken over validation. That makes pattern a good first net for fixed formats, with your JavaScript checks as the second net for rules that involve other fields or that need clearer messages. As with all built-in checks, it checks shape, not truth: a four-digit code that matches the pattern may still be the wrong code, and only checking it against real records can prove otherwise. Shape, stated plainly to the visitor and enforced simply, is pattern's proper job.

A pattern should also be tested with values that are close to correct, such as a code with a letter where a digit is expected, or a code with a space in the middle, because those near-misses are the values real visitors will type. A pattern that accepts a near-miss will let a wrong code through, and a pattern that rejects a valid variation, such as a code with a hyphen that the system actually accepts, will block a correct visitor. Testing near-misses, not only clear passes and clear failures, is what makes a pattern trustworthy in practice.

Check: What does pattern="[0-9]{4}" require a value to be?

Section 42 — Min, max and step

For number, date, and time inputs, three attributes define the allowed range and granularity: min, max, and step. The min attribute sets the smallest allowed value, and max sets the largest. A number input with min="1" and max="10" should hold a value between 1 and 10, and a date input with a min of today should not accept yesterday. The step attribute sets the jump between allowed values, measured from the minimum or from a default base. A number input with min="0" and step="5" allows 0, 5, 10, 15, and so on, and a value such as 7 would be flagged as a step mismatch. For date inputs, step is measured in days, so step="7" allows weekly dates. For time inputs, step is measured in seconds. These attributes shape the pickers and spinners the browser offers, so a visitor stepping through values with the arrows will land only on allowed ones.

As with other built-in checks, min, max, and step guide and check, but they do not guarantee. A visitor can type a value directly into a number input in many browsers, bypassing the spinner's stepping, and the browser's step check may or may not catch it depending on the browser and on whether the form is submitted normally or handled by JavaScript with novalidate. Your JavaScript should therefore re-check the range and the step, using the same logic as Section 32 for numbers and Section 33 for dates. That duplication is not wasteful; it is defence in depth, with the attributes providing immediate guidance and your code providing the final guarantee. It also protects against stale attributes: a date minimum hardcoded as a past date, for example, will quietly allow past dates once that date passes, while a JavaScript check computed from the current date stays correct.

Setting these attributes dynamically is a useful technique. A booking form can set its date input's minimum to today's date when the page loads, by building the year-month-day string in JavaScript and assigning it to the input's min property. A quantity input's maximum can be set from the stock available, fetched from a server in Chapter 22's style, so the form never offers more than exists. When you set these attributes in code, remember that they are strings in the HTML sense, but properties in JavaScript, and that assigning to the property updates the browser's checks immediately. Min, max, and step are small attributes with a large effect on usability: they turn an open-ended field into a bounded one, show the visitor the boundaries before they cross them, and give your validation a clear, stated rule to enforce.

For a time input, step works in seconds, so a step of 900 allows times every fifteen minutes, which suits appointment booking, where slots are quarter-hourly. Setting that step, and stating the slot length in the label, guides the visitor to choose a valid time without trial and error, and it prevents bookings at odd times, such as 10:07, that the service cannot honour. Step is therefore not just a validation rule; it is a way to express the service's real constraints in the field itself.

Check: With min="0" step="5", which values are allowed?

Section 43 — The Constraint Validation API

So far this chapter has treated built-in validation as something the browser does at submission time, largely out of your sight. The Constraint Validation API brings that machinery into your JavaScript, so you can ask the browser what it thinks about a field, rather than guessing or duplicating its checks. Every form field in a modern browser carries a validity object, a set of true-or-false flags describing exactly what is wrong, if anything. The flag valueMissing is true when a required field is empty. typeMismatch is true when an email or URL field's value does not match its type's shape. patternMismatch is true when a value fails its pattern. rangeUnderflow and rangeOverflow flag values below the minimum or above the maximum, and stepMismatch flags a value that does not fit the step. tooLong and tooShort relate to length limits, and customError is true when you have set a custom message, as Section 45 explains. Together, these flags give a precise diagnosis for any field.

The API also gives you the field's overall verdict. The property field.validity.valid is true when every flag is false, meaning the field passes all its built-in checks, and false when any flag is true. The method field.checkValidity() returns the same verdict as a boolean, and form.checkValidity() returns true only when every field in the form passes. The method field.validationMessage holds the message the browser would show for the field's current failure, in the browser's own wording, which is useful for debugging even if you prefer to show your own messages to visitors. Using these properties, you can write validation that leans on the browser's checks instead of reimplementing them: ask each field whether it is valid, and if not, ask its validity object which flag is set, and show a message tailored to that flag. A valueMissing failure gets "Please enter your name"; a typeMismatch on an email field gets "Please enter a valid email address". That mapping turns generic failures into specific guidance.

There is a deeper benefit to this approach. Because the browser's checks and your checks now share the same definitions, they cannot drift apart. If you change a field's pattern in the HTML, the browser's pattern check and your validity-based message update together, because both read the same attribute. Hand-written checks, by contrast, can drift: a pattern in the HTML and a regex in the JavaScript that were once identical can diverge after an edit to one but not the other, producing a form that accepts a value in one place and rejects it in another. The Constraint Validation API is therefore not just a convenience; it is a way to keep validation single-sourced. Define the rules in the HTML where the browser can enforce them, and use the API in JavaScript to read the results and present them well.

Check: What does field.validity.valueMissing mean?

Section 44 — checkValidity and reportValidity

The Constraint Validation API offers two methods that sound similar and do different things, and confusing them is a common source of bugs. The method form.checkValidity() checks every field in the form against its built-in rules and returns true if all pass, or false if any fail. It does not show anything to the visitor. It is a silent question: "Would this form pass?" The method form.reportValidity() asks the same question, but if the answer is no, it also shows the browser's messages for the failing fields, focusing the first failing field so the visitor can fix it. It is the same question with the answer displayed. Both methods return a boolean, so either can be used in an if statement, but only the second changes what the visitor sees.

if (!form.checkValidity()) {
  // show your own messages here
}

Choosing between them depends on whose messages you want the visitor to see. If you are building custom error display, with messages beside each field in your own wording and styling, use checkValidity() to learn whether the form passes, then walk the fields yourself, reading their validity flags and showing your own messages. Calling reportValidity() in that case would duplicate your messages with the browser's, showing two sets of feedback for the same failure. If, on the other hand, you are happy with the browser's messages and styling, use reportValidity() and let the browser do the display work. That is a perfectly respectable choice for a simple form, and it requires almost no error-display code at all. The same pair exists on individual fields: field.checkValidity() and field.reportValidity() check and optionally display for one field, which is useful for blur validation, where you want to check and show for just the field the visitor left.

There is one more detail that matters in practice. Both methods respect the form's novalidate attribute in the sense that they still perform the checks and return the verdict, even though novalidate stops the browser from blocking submission automatically. That means you can use novalidate to take over display, while still using checkValidity() to borrow the browser's checking logic, which is a clean division of labour: the browser checks, you display. Conversely, if you call reportValidity() on a form with novalidate, the browser will still show its messages when you ask it to, because you asked explicitly. Understanding that distinction, silent check versus check with display, lets you combine built-in checking and custom display without duplication, and it is the key to making the Constraint Validation API feel like a tool rather than a mystery.

A practical use for checkValidity on a single field is in a blur handler, where you want to know whether just that field passes, without checking the whole form. Calling field.checkValidity in the blur handler tells you whether to show or clear that field's error, without disturbing other fields, and it reuses the browser's rules for that field, so the blur check and the submit check cannot disagree. That consistency, one set of rules used in both places, is the benefit of leaning on the browser's checking logic wherever you can.

Check: What is the difference between checkValidity() and reportValidity()?

Section 45 — setCustomValidity

Built-in checks cover emptiness, shape, range, and pattern, but some rules are specific to your form and cannot be expressed as an attribute. Two passwords must match. A username must not already be taken. An end date must follow a start date. For these custom rules, the Constraint Validation API provides setCustomValidity(message). Calling it with a message marks the field as invalid and stores that message as its validation message. Calling it with an empty string clears the custom error and restores the field to validity, as far as custom checks are concerned. The field's validity.customError flag is true while a custom message is set, and checkValidity() will return false for a form containing such a field, so custom rules integrate cleanly with the built-in machinery instead of sitting beside it as a separate system.

if (password.value !== confirm.value) {
  confirm.setCustomValidity("Passwords do not match.");
} else {
  confirm.setCustomValidity("");
}

The most common bug with setCustomValidity is forgetting to clear it. Once set, a custom message persists until it is explicitly cleared with an empty string, even if the visitor fixes the problem. A password confirmation field that was once mismatched will remain invalid forever, blocking submission, unless your code clears the message when the values match again. The safe pattern is to re-run the custom check whenever either related field changes, setting the message when the rule fails and clearing it when the rule passes, so the field's custom state always reflects the current values. Listening for input events on both password fields and running the comparison in both handlers achieves this. The same pattern applies to cross-field date checks and to any other rule involving more than one field: check on change, set or clear, and never leave a stale custom error standing.

Custom validity also interacts with display in a useful way. Once a custom message is set, calling reportValidity() will show that message with the browser's styling, and validationMessage will return it, so your custom rule benefits from the same display machinery as built-in rules. If you prefer your own display, you can read validity.customError and show your own message beside the field, just as you would for a built-in flag. Either way, the rule is defined once, in the check that sets or clears the message, and both the verdict and the display follow from it. That single-source property, shared with the rest of the Constraint Validation API, is what makes setCustomValidity preferable to a parallel system of boolean flags and hand-written messages: there is one notion of validity, the browser's, and your custom rules join it rather than competing with it.

Custom validity is also useful for a username availability check, where the rule depends on data from a server, not just on the field's own value. When the server reports that a username is taken, your code can set a custom message on the field, such as 'That username is already taken', making the field invalid until the visitor chooses another. Clearing that message when the field changes, and re-checking availability for the new value, keeps the custom state in step with the visitor's typing, as with any other custom rule.

Check: How do you clear a custom validity message once the problem is fixed?

Section 46 — novalidate when JavaScript takes over

The novalidate attribute, added to a form element, switches off the browser's automatic validation at submission time. With novalidate present, the browser will not block submission when a required field is empty or a value fails its type or pattern check, and it will not show its own messages. Instead, submission proceeds to your submit handler, which is then fully responsible for deciding whether the data is acceptable. This is the right choice when you are deliberately replacing the browser's messages with your own, for example to show errors beside each field in your own wording and styling, or to implement rules the browser cannot express. In that case, the browser's messages would duplicate or clash with yours, and switching them off gives you a clean canvas.

<form novalidate></form>

It is important to understand what novalidate does not do. It does not switch off validation itself; it switches off the browser's automatic enforcement and display of it. The Constraint Validation API still works: checkValidity() still checks the fields and returns a verdict, validity flags still reflect the rules, and reportValidity() will still show messages when you explicitly ask it to. So a form with novalidate is not an unvalidated form; it is a form whose validation is driven by your code, borrowing the browser's checking logic where that suits you. That distinction is why novalidate is safe in the hands of a developer who validates in the submit handler, and dangerous in the hands of one who adds it and then validates nothing. The attribute removes a safety net, and you must replace it with your own before you remove it.

When should you use novalidate, and when should you leave it off? Leave it off for simple forms where the browser's messages are acceptable, because then you get validation with no code at all, including the case where your JavaScript fails to load. Add it when you need full control over error display, when you have custom rules that the browser's messages would confuse, or when you are progressively enhancing a form and want one consistent error style across all failures. Never add it as a quick fix for a validation bug, such as a form that will not submit and you cannot see why; that hides the symptom while leaving the underlying rule broken or misunderstood. And never add it to skip validation altogether, for example to rush a demo: a form that accepts anything is not a finished form, it is a form with its checks removed, and the data it collects will eventually cause problems downstream that are far harder to fix than a validation message.

A form with novalidate should still be tested with JavaScript disabled, to confirm that it degrades sensibly, rather than silently accepting anything. With JavaScript off, a novalidate form will submit unchecked, which is a risk if the server does not validate, so the server's own checks become the final safety net in that case. Knowing that degradation, and ensuring the server validates, is part of using novalidate responsibly, rather than assuming JavaScript will always be present to do the checking.

Check: When is novalidate the right choice, and when is it not?

Section 47 — Styling valid and invalid fields

CSS provides two pseudo-classes that match fields according to their validation state: :valid matches a field that passes its built-in checks, and :invalid matches one that fails them. With a single rule, you can give invalid fields a red border, valid fields a green one, and let the visitor see at a glance which fields need attention. That visual feedback is immediate and requires no JavaScript, because the browser updates the pseudo-classes as the field's state changes. It is a powerful tool, but it has a fairness problem that you need to understand before you use it, because a field's initial state is often invalid through no fault of the visitor.

input:invalid { border-color: red; }

The problem is that an empty required field is invalid from the moment the page loads. A form styled with a plain :invalid rule will therefore display every required field with a red border before the visitor has typed anything, which reads as a page full of errors before the visitor has had a chance to do anything right. That is discouraging and misleading: the visitor has not made a mistake, they have simply not started yet. Browsers and developers have developed several answers to this. One is to combine :invalid with :focus or with a class added after the field has been visited, so the red border only appears once the visitor has interacted with the field. Another is to apply the styling only after the form has been submitted once, by adding a class to the form in the submit handler and scoping the rule under that class, such as form.submitted input:invalid. Modern CSS also offers :user-invalid, which matches a field only after the visitor has interacted with it and it remains invalid, which captures the intended behaviour directly, though support and exact behaviour vary by browser, so test it in your target browsers before relying on it alone.

Beyond timing, there are presentation rules to respect. Colour must not be the only signal, for the accessibility reasons given in Section 34: pair the border colour with a written message, an icon, or both, so a visitor who cannot distinguish the colour still knows the field failed. And keep the styling restrained: a red border and a message are feedback, while a shaking field, a loud colour, and a large icon together are alarm. The goal of validation styling is to guide attention to the fields that need it, at the moment the visitor is ready to fix them, not to punish the visitor for the state of an untouched form. Used with a visited or submitted qualifier, :valid and :invalid give you that guidance with almost no code, and they keep the visual state and the validation state in agreement automatically, because both come from the same browser machinery.

Check: Why might styling :invalid from the start be unfair to the visitor?

Section 48 — Accessible error messages

A form's error messages must reach every visitor, including those who cannot see the screen and those who navigate by keyboard. That requirement shapes how errors are written into the page. The first technique is to link each error message to its field with aria-describedby. The field carries an attribute pointing at the message element's id, and a screen reader then announces the message along with the field, so the visitor hears the problem in context, without having to search the page for it. The message element itself should be a plain paragraph with a stable id, present in the page from the start or added when the error appears, and the aria-describedby link should be present whether or not an error is currently shown, so the relationship is established before it is needed.

<input id="email" aria-describedby="email-error">
<p id="email-error">Please enter a valid email address.</p>

The second technique concerns messages that appear after an action, such as submission. A visitor using a screen reader may not notice a message that appears elsewhere on the page, because their focus is on the submit button, not on the field that failed. Giving the message container role="alert" makes it a live region that is announced as soon as its content changes, without moving focus. Alternatively, moving focus to the first failing field on submit achieves a similar effect by a different route: the visitor's attention is taken to the problem, and the field's label and described-by message are announced together. Many well-designed forms do both: announce the summary, and move focus to the first error, so the visitor is neither left unaware nor left to hunt. Whichever route you choose, test it with a keyboard alone, without a mouse, because keyboard navigation reveals focus problems that mouse testing hides: a message that appears but never receives focus, or a focus order that jumps unpredictably after an error, will strand a keyboard visitor.

There are also writing rules for accessible errors. Messages should be specific, naming the field and the fix, as Section 27 argues, because a screen reader user hears messages one at a time and cannot skim a list as quickly as a sighted visitor can scan it. Messages should not rely on colour alone, as Section 47 notes, and they should not be written in all capitals or with excessive punctuation, which screen readers may announce awkwardly. Finally, success deserves the same care as failure: a success message should also be announced, or focus should move to it, so a visitor who cannot see the page knows their submission worked. Accessibility in forms is not a separate feature added at the end; it is the same good practice, messages beside fields, specific wording, and attention to focus, applied with the awareness that not every visitor experiences the page visually.

Check: What does aria-describedby do for an error message?

Section 49 — Disabling submit until valid, and its pitfalls

It is tempting to keep a form's submit button disabled until every field is valid. The logic seems sound: if the form cannot be submitted while it is invalid, then invalid data can never be sent, and the visitor cannot make the mistake of submitting too early. In practice, this pattern has serious drawbacks, and it is worth understanding them before you adopt it, because they affect whether visitors can complete your form at all. The central problem is that a disabled button explains nothing. A visitor faced with a grey, unclickable button receives no information about what is wrong, which field is blocking submission, or what they need to change. They may re-read the form, find nothing obviously wrong, and conclude the form is broken, abandoning a task they were nearly able to complete.

The problem is worse for some visitors than others. A disabled button is often styled with low contrast, making it hard to see. Screen readers may announce it as unavailable without explaining why, leaving a visitor who cannot see the form with no path forward. A visitor who filled the form by autofill may not have triggered the validation events your enable logic listens for, leaving the button disabled despite a complete form. And a form with a cross-field rule, such as matching passwords, may keep the button disabled for a reason that is not visible in any single field, so even a careful visitor cannot diagnose it. Each of these cases turns a guard into a barrier, and the barrier falls hardest on the visitors who already face the most friction.

If you use this pattern, you must mitigate its flaws. Show, at all times, what still needs fixing: a list of outstanding requirements near the button, updated as fields are completed, so the visitor can see the path to an enabled button. Keep the button's disabled styling clearly visible, not faded to invisibility. And consider the alternative that many designers prefer: leave the submit button enabled, and let the submit check, Section 38, do its job. A working button plus clear errors on submit gives the visitor a way to ask "is this ready?" and to receive a specific answer, which is a conversation, while a disabled button gives them silence. The submit check is the safety net that makes the disabled-button guard unnecessary, and a form that trusts its submit check is usually the kinder and more robust design.

If a disabled submit button is used, its disabled appearance should still be clearly visible, not faded to near-invisibility, because a button the visitor cannot see is a button they cannot understand. Low-contrast disabled styling, common in some designs, makes the button hard to find, especially in bright light or for visitors with low vision. Keeping the button visible, with a clear label, even while disabled, and pairing it with a list of outstanding requirements, turns a silent barrier into a visible checklist the visitor can work through.

Check: What is the main problem with a submit button that stays disabled with no explanation?

Section 50 — Putting it together: the submit flow

A solid form handler follows the same steps every time, and learning that sequence by heart will serve you for every form you build. It begins with preventing the default submission, as in Section 21, so the browser does not navigate away before your code has judged the data. It continues by reading the values, trimming text fields, and converting numbers and dates to their proper types, because validation must judge the values as they will be used, not as raw strings with accidental spaces. It then checks each field in turn, using the field's own rules: emptiness, length, shape, range, and any custom rules such as matching passwords, collecting every failure rather than stopping at the first, so the visitor can fix everything in one pass. Each failure is shown beside its own field, in a specific message that names the field and the fix, and the handler stops there, sending nothing.

Only when every field passes does the handler proceed to the next stage: using or sending the data. In this chapter's exercises, that stage might be showing a success message and logging the data. In a real application, it is usually sending the data to a server with fetch, as Chapter 22 will show, and handling the server's reply, including the possibility that the server rejects data your front-end checks accepted. That server check is the final net, and a complete handler must handle its failure too, showing a message and preserving the visitor's input so they do not have to retype it. After a successful submission, the handler shows a clear success message, stating what happened, such as "Your message has been sent", and resets the form with form.reset(), clearing the fields and their errors, so the form is ready for a new submission and does not look as if the old data is still waiting to be sent.

This flow is worth memorising as a checklist, because it is easy to skip a step under pressure and hard to notice the omission afterwards. Prevent, read, trim and convert, check everything, show errors beside fields and stop, send only on success, handle the server's reply, confirm success, and reset. A form that follows those steps is predictable for the visitor and trustworthy for the developer: the visitor always knows whether their submission worked and what to fix if it did not, and the developer always knows that data leaving the page has passed every check. The practical task below asks you to build exactly such a form, and the chapter questions test whether you can explain each step and why it is there. Forms are where front-end code meets real people, and a reliable submit flow is how you show those people respect.

Check: List the steps of a solid submit handler in order.

Practical task

Build a sign-up form with fields for name, email, password and password confirmation, plus a checkbox for agreeing to terms. Add labels for every field. Write JavaScript that prevents the default submit, trims the name, checks that no field is empty, that the email has an @ sign and a dot, that the password is at least 8 characters, that the two passwords match, and that the terms box is ticked. Show each error beside its own field, clear an error when that field is edited, and on success show a success message and reset the form. Test it with empty fields, a bad email, mismatched passwords and a fully correct entry.

Chapter questions

  1. Why should you listen for the submit event on the form rather than a click on the submit button?
  2. Explain the difference between validating on input, on blur and on submit, and when each is most useful.
  3. Write JavaScript that checks a username is between 3 and 20 characters long and shows a clear error message if it is not.
  4. What is the difference between checkValidity() and reportValidity(), and when would you use each one?
  5. A visitor using a screen reader submits your form and an error appears. How do you make sure they learn about it?