Suggest an editImprove this articleRefine the answer for “HTML forms and built-in validation”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**HTML form validation** - `<form>` groups controls like `<input>` and `<select>` and handles data submission; built-in validation runs in the browser using `required`, `type="email"`, and `pattern` before any server request is made. ```html <input type="email" name="email" required> <!-- Blocks empty submission and wrong format --> ``` **Key point:** browser validation runs client-side only. Always re-validate on the server.Shown above the full answer for quick recall.Answer (EN)Image**HTML forms** collect user input through controls like `<input>`, `<select>`, and `<textarea>` inside a `<form>` element. Built-in validation checks that data against rules like `required` or `pattern` before the browser sends the request. ## Theory ### TL;DR - Form = a container with `action` (where to send) and `method` (POST/GET); inputs are the fields users fill - Built-in validation runs without JavaScript via `required`, `type="email"`, `min`/`max`, `pattern` - Browser blocks submit and shows a tooltip if any field fails; `:invalid` CSS pseudo-class activates at the same time - Always re-validate on the server - browser checks are bypassed by bots and direct HTTP requests - Add `novalidate` to skip browser tooltip UI while still running `checkValidity()` in code ### Quick example ```html <form action="/submit" method="POST"> <label for="email">Email:</label> <input type="email" id="email" name="email" required> <!-- Empty submit: "Please fill out this field" --> <!-- Bad format: "Please include an '@' in the email address" --> <label for="age">Age (18+):</label> <input type="number" id="age" name="age" min="18" required> <!-- Age 17: "Value must be greater than or equal to 18" --> <button type="submit">Send</button> </form> ``` Two attributes do all the work: `type="email"` tells the browser what format to expect, `required` blocks empty submission. No JavaScript needed. ### Form elements | Element | What it does | |---|---| | `<input>` | Text, email, password, checkbox, radio, number, tel, range, date | | `<textarea>` | Multi-line text | | `<select>` | Dropdown | | `<fieldset>` | Groups related controls | | `<label>` | Ties text to a control (click label = focus input) | | `<datalist>` | Autocomplete suggestions for `<input>` | ### Validation attributes Each attribute maps to a specific check the browser runs before submit: ```html <input type="text" required> <!-- blocks empty --> <input type="email"> <!-- checks for @ and domain --> <input type="number" min="1" max="100"> <!-- numeric range --> <input type="text" minlength="3" maxlength="50"> <!-- string length --> <input type="text" pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, underscore, hyphen only"> <!-- regex match --> <input type="tel" pattern="\d{4}-\d{4}-\d{4}-\d{4}"> <!-- card format --> ``` The `title` attribute sets the tooltip text shown when `pattern` fails. Without it, browsers show a generic message. ### How validation works internally Browsers parse form attributes into a `ValidityState` object on each input. You can read it directly: ```js const input = document.querySelector('#email'); console.log(input.validity.valueMissing); // true if empty + required console.log(input.validity.typeMismatch); // true if wrong format console.log(input.validity.valid); // true only if all checks pass ``` On submit, the browser calls `checkValidity()` on every field. If any returns `false`, it stops the request, focuses the first bad field, and shows a native tooltip. No server request happens. The `:invalid` CSS pseudo-class activates at the same time. ### CSS for validation states ```css input:valid { border-color: green; } input:invalid { border-color: red; } /* Don't mark empty fields red on page load */ input:invalid:not(:focus):not(:placeholder-shown) { border-color: red; } ``` That last selector solves a common UX problem. Without it, all `required` fields show red borders immediately on page load before the user touches anything. ### novalidate and custom error UI Add `novalidate` to the form when you want to handle errors yourself (toasts, inline messages) but still run checks in code: ```html <form id="payment" novalidate> <input type="tel" name="card" pattern="\d{4}-\d{4}-\d{4}-\d{4}" required> <button type="submit" disabled>Pay</button> </form> <script> const form = document.getElementById('payment'); const btn = form.querySelector('button'); form.addEventListener('input', () => { btn.disabled = !form.checkValidity(); }); form.addEventListener('submit', (e) => { if (!form.checkValidity()) { e.preventDefault(); form.querySelectorAll(':invalid').forEach(field => { // show your own error UI here }); } }); </script> ``` `novalidate` skips the browser tooltip on submit. `checkValidity()` still does the real validation work in code. ### Common mistakes **Trusting client-side checks alone** Browser validation is invisible to the server. A bot can POST directly with curl and skip every `required` attribute you wrote. Always re-check server-side. ```js // Express.js - mirror the HTML rules on the server if (!req.body.email.includes('@')) { return res.status(400).json({ error: 'Invalid email' }); } ``` **`required` on hidden inputs** Hidden fields with `required` always fail validation because their value is `""`. This breaks multi-step wizards silently. ```html <!-- Wrong: always blocks submit --> <input type="text" name="step" required style="display:none" value=""> <!-- Fix: hidden inputs don't use required --> <input type="hidden" name="step" value="2"> ``` **Forgetting the `name` attribute** Without `name`, the field is invisible to `FormData` and never reaches the server. I've caught this in code reviews more often than any other form bug. ```html <!-- Wrong: field submits nothing --> <input id="username" required> <!-- Fix --> <input id="username" name="username" required> ``` **Calling `setCustomValidity('')` unconditionally** Clearing the custom message before checking wipes real errors too. ```js // Wrong: clears the error on every input event regardless input.setCustomValidity(''); // Fix: clear only when the field is actually valid input.setCustomValidity(input.validity.valid ? '' : 'Custom error message'); ``` ### Real-world usage - React Hook Form: `register('email', { required: true })` maps to native `required` on the input element - Stripe card forms use `pattern` and `:invalid` for real-time format feedback before the SDK takes over - Common production setup: `novalidate` on the form with `express-validator` mirroring the rules server-side - `formnovalidate` on a "Save Draft" button lets users save incomplete forms without triggering validation ### Follow-up questions **Q:** What happens when form submission fails validation? **A:** The browser cancels the request, applies `:invalid` styles, focuses the first bad field, and shows a native tooltip. You can also trigger this manually with `form.reportValidity()`. **Q:** What is the difference between `required` and `minlength="1"`? **A:** `required` blocks empty values including whitespace-only strings. `minlength="1"` allows empty submission but blocks a short non-empty value. **Q:** Can validation run on blur instead of only on submit? **A:** Yes. Add `input.addEventListener('blur', () => input.reportValidity())` to show errors the moment a user leaves a field. **Q:** What does `novalidate` actually do? **A:** It tells the browser to skip native tooltip UI on submit. It does not disable `checkValidity()` or `:invalid` styles. Senior answer: use it when you need your own error rendering like toasts or modals, while keeping programmatic checks active. **Q:** How does built-in validation interact with `FormData`? **A:** `FormData` is built only when the browser decides to send the form. If validation blocks submit, `FormData` is never created and no network request is made. ## Examples ### Basic: Registration form with built-in checks ```html <form action="/api/users" method="POST"> <label for="username">Username (3-20 chars):</label> <input type="text" id="username" name="username" required minlength="3" maxlength="20" pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, underscore, hyphen only"> <label for="email">Email:</label> <input type="email" id="email" name="email" required> <label for="password">Password (min 8 chars):</label> <input type="password" id="password" name="password" required minlength="8"> <button type="submit">Create Account</button> </form> <style> input:valid { border: 2px solid green; } input:invalid { border: 2px solid red; } </style> ``` Username `"ab "` fails the `pattern` (spaces are not in the character class) and shows "Letters, numbers, underscore, hyphen only". Valid input gets a green border and the form POSTs all three fields via `FormData`. ### Intermediate: GitHub-style signup with real-time feedback ```html <form id="signup" action="/api/register" method="POST" novalidate> <fieldset> <legend>Create your account</legend> <label for="user">Username:</label> <input type="text" id="user" name="username" required minlength="3" maxlength="39" pattern="[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?"> <span class="error" id="user-error"></span> <label for="mail">Email:</label> <input type="email" id="mail" name="email" required> <button type="submit">Sign up</button> </fieldset> </form> <script> const form = document.getElementById('signup'); form.addEventListener('submit', (e) => { if (!form.checkValidity()) { e.preventDefault(); form.querySelectorAll(':invalid').forEach(field => { const errorEl = document.getElementById(field.id + '-error'); if (errorEl) errorEl.textContent = field.validationMessage; }); } }); form.querySelectorAll('input').forEach(input => { input.addEventListener('input', () => { const errorEl = document.getElementById(input.id + '-error'); if (errorEl && input.validity.valid) errorEl.textContent = ''; }); }); </script> ``` `novalidate` removes the browser tooltip. `field.validationMessage` is a browser-generated string like "Please include an '@' in the email address" - you get the native error text without writing your own copy. ### Advanced: Payment form with disabled submit until valid ```html <form id="checkout" novalidate> <label for="card">Card number (XXXX-XXXX-XXXX-XXXX):</label> <input type="tel" id="card" name="card" pattern="\d{4}-\d{4}-\d{4}-\d{4}" required> <label for="tip">Tip (10-30%):</label> <input type="range" id="tip" name="tip" min="10" max="30" value="15"> <button type="submit" id="pay-btn" disabled>Pay</button> </form> <script> const form = document.getElementById('checkout'); const btn = document.getElementById('pay-btn'); function syncButton() { btn.disabled = !form.checkValidity(); } form.addEventListener('input', syncButton); form.addEventListener('invalid', (e) => { e.preventDefault(); e.target.setCustomValidity('Card must be formatted as XXXX-XXXX-XXXX-XXXX'); syncButton(); }, true); // capture phase - fires on every invalid field, not just the form </script> ``` The `invalid` event listener uses `true` as the third argument to capture it in the capture phase, so it fires on every field. The button stays disabled until `form.checkValidity()` returns `true`. This pattern is what Stripe-style forms use before handing off to a payment SDK.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.