Skip to main content
PProfileMatch

HTML Forms and Built-In Validation Without JavaScript

Web DevelopmentLesson 3 of 45 min read

What you'll learn

  • Build a form with properly connected labels and the correct input types
  • Use required, minlength, pattern, min, max and step to validate input in the browser
  • Group radio buttons with fieldset and legend and choose between GET and POST
  • Explain what a form actually sends and why server-side validation is still required

On this page

  1. A form is a bundle of named values
  2. Choose the input type that matches the data
  3. What happens when the button is pressed
  4. Customising messages with a little JavaScript
  5. Common mistakes
  6. How this shows up in real projects and interviews

A bike workshop wants online bookings. Their first version is a text box where people type everything: name, bike, date, phone. Half the bookings arrive as "tomorrow-ish" and a quarter have a phone number with a letter in it. Browsers can already prevent most of this without a single line of JavaScript, if you tell them what each field should contain. This lesson builds that booking form and then shows exactly what leaves the browser when someone presses the button.

A form is a bundle of named values

The form element groups controls and says where to send them (action) and how (method). Each control contributes a pair of name and value when the form is submitted. A control with no name sends nothing, which is the single most common reason a form "does not work". Every control also needs a visible label connected to it, either with for matching the control's id, or by wrapping the control in the label.

booking.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Book a Bike Service | Spoke &amp; Chain Workshop</title>
</head>
<body>
  <h1>Book a bike service</h1>

  <form action="/bookings" method="get">
    <p>
      <label for="customer">Full name</label><br>
      <input id="customer" name="customer" type="text" required minlength="2" autocomplete="name">
    </p>

    <p>
      <label for="email">Email</label><br>
      <input id="email" name="email" type="email" required autocomplete="email">
    </p>

    <p>
      <label for="phone">Mobile (10 digits)</label><br>
      <input id="phone" name="phone" type="tel" pattern="[0-9]{10}"
             title="Exactly ten digits, no spaces" autocomplete="tel-national">
    </p>

    <fieldset>
      <legend>Type of bike</legend>
      <label><input type="radio" name="bike" value="road" required> Road</label>
      <label><input type="radio" name="bike" value="mountain"> Mountain</label>
      <label><input type="radio" name="bike" value="ebike"> E-bike</label>
    </fieldset>

    <p>
      <label for="date">Drop-off date</label><br>
      <input id="date" name="date" type="date" min="2026-01-01" required>
    </p>

    <p>
      <label for="parts">Number of new tyres</label><br>
      <input id="parts" name="tyres" type="number" min="0" max="4" step="1" value="0">
    </p>

    <p>
      <label for="service">Service level</label><br>
      <select id="service" name="service" required>
        <option value="">Choose one</option>
        <option value="basic">Basic check</option>
        <option value="full">Full overhaul</option>
      </select>
    </p>

    <p>
      <label for="notes">Anything we should know?</label><br>
      <textarea id="notes" name="notes" rows="3" maxlength="300"></textarea>
    </p>

    <p>
      <label><input type="checkbox" name="collect" value="yes"> Collect from my home</label>
    </p>

    <button type="submit">Request booking</button>
  </form>
</body>
</html>

You see a heading, then each label on its own line with the control below it, a bordered box with a caption (the fieldset and its legend) holding three side-by-side radio options, a date picker, a number box with a small spinner, a drop-down that starts on "Choose one", a three-row text area, a checkbox and a button. Clicking a label focuses or toggles its control, which gives touch users a bigger target. The empty-valued first option is what makes required meaningful on the select.

Choose the input type that matches the data

The type attribute changes the keyboard shown on phones, the widget drawn, and the validation applied. An email field checks for a plausible address, a date field offers a calendar and produces a YYYY-MM-DD value, and number rejects letters. Note that tel does not validate anything on its own because phone formats vary worldwide; that is why we add pattern.

AttributeApplies toWhat it enforces
requiredMost controlsA value must be provided
minlength / maxlengthText, textareaCharacter count limits (min is checked when the user edits)
patternText-like inputsValue must match a regular expression in full
min / max / stepNumber, date, rangeNumeric or date bounds and allowed increments
type="email" / "url"InputValue must look like an address or URL
novalidateFormTurns off built-in checks (use for custom validation UI)

What happens when the button is pressed

Before anything is sent, the browser runs constraint validation. If a control is invalid, submission stops, the first bad control gets focus, and a message appears next to it, for example "Please fill out this field" or "Please match the requested format" (your title text is appended to that second message). If everything is valid, the browser sends the data. With method="get" the pairs travel in the URL; with method="post" they travel in the request body, which you should use for anything that changes data on the server or is private.

Resulting request for the booking form (method=get)
GET /bookings?customer=Ravi+Menon&email=ravi%40example.com&phone=9876543210&bike=road&date=2026-03-14&tyres=2&service=full&notes=&collect=yes

Output

Spaces become +, @ becomes %40.
An unchecked checkbox sends nothing at all.
A field without a name attribute is never included.
Disabled controls are not submitted either.

Customising messages with a little JavaScript

Built-in messages are generic. The Constraint Validation API lets you replace them when you need to, while the browser still does the checking. In the snippet below, the custom message is cleared on every input so the field can become valid again, which is the step people forget.

Custom message for the phone field
<script>
  const phone = document.getElementById("phone");

  phone.addEventListener("input", () => {
    phone.setCustomValidity("");              // reset first
    if (phone.validity.patternMismatch) {
      phone.setCustomValidity("Enter the 10 digits only, like 9876543210.");
    }
  });
</script>

You can also style states with CSS: input:invalid matches any invalid control, even before the user has typed. Recent browsers add :user-invalid, which only matches after the user has interacted, and that is usually what you want for red borders. Check browser support before relying on it for anything critical.

Common mistakes

  • Missing `name`. The control looks fine but the value never reaches the server; the request simply lacks that key.
  • Placeholder instead of label. The hint disappears as soon as typing starts, has poor contrast by default, and is not a reliable accessible name. Use a real label.
  • `button` without a type inside a form. The default is submit, so a "Cancel" or "Show password" button submits the form unexpectedly. Add type="button".
  • Different `name` on each radio. Radios only form a group when they share the same name; otherwise several can be selected at once.
  • Pattern that does not match the whole intent. pattern="[0-9]{10}" already anchors to the full value, so adding ^ and $ is redundant, but forgetting a title leaves users with an unexplained "match the requested format".
  • Duplicate `id` values. A label's for links to the first match, so two fields with id="name" make the second label focus the wrong control.

How this shows up in real projects and interviews

Even if you later use a form library, the browser's rules stay underneath, and people who know them write less code. Interview questions include "what is the difference between GET and POST", "how do you validate without JavaScript", and "why validate again on the server". In React the same ideas appear as controlled inputs; compare them in React Events and Forms. Accessibility reviewers will also check that every field has a label, which leads into the next lesson.

Try it yourself

Create a sign-up form for a community garden waitlist with: name, email, plot size (radio group in a fieldset), number of people in the household (number from 1 to 8), and a consent checkbox that must be ticked. Make it work with method="post" to a dummy action. Then open DevTools, remove the required attribute from one field, and submit to prove why the server still needs to check.

Show a hint

A required checkbox must be ticked before submission. In DevTools, right-click the input, choose Edit as HTML, and delete the attribute.

Key takeaways

  • A control only submits if it has a name, and each one needs a visible, connected label.
  • Pick the type that matches the data, then add required, pattern, min, max and minlength for browser-side checks.
  • GET puts data in the URL, POST in the body; unchecked checkboxes and disabled controls send nothing.
  • Always validate again on the server; HTML validation is only a convenience for honest users.

Test yourself

A trick from this lesson's ride: No name, no show: a form control only travels if it has a name. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

How do I validate a form in HTML without JavaScript?

Use attributes on the controls: required, type="email", pattern, minlength, maxlength, min and max. The browser checks them on submit, blocks invalid submission and shows its own message. Still re-check everything on the server.

What is the difference between GET and POST in an HTML form?

GET appends the field values to the URL, so results can be bookmarked and shared, which suits searches. POST sends them in the request body, which suits actions that change data or involve private values. Neither is secure by itself; use HTTPS and server-side checks.

Why is my HTML form not submitting any data?

Check that each control has a name attribute, that it sits inside the form element, and that it is not disabled. Also check whether a validation message is blocking the submission, and whether a button type="button" is being used instead of a submit button.