The browser already knows how to validate most of what a form needs — required fields, email format, number ranges, text length, custom patterns — with zero JavaScript. The gap most developers hit isn't a missing feature; it's a genuinely non-obvious interaction between that built-in validation and a hidden attribute, one that quietly breaks multi-step and conditional forms in a way that's easy to ship without noticing.
Quick Answer
Use native HTML5 attributes first — required, type="email"/type="url", minlength/maxlength, min/max, and pattern — before writing any custom JavaScript validation. Call form.reportValidity() to trigger the browser's built-in error UI programmatically, and input.setCustomValidity() for messages the built-in attributes can't express. The one trap: a required field hidden behind a hidden attribute (a later step, a conditionally-hidden section) still blocks submission unless you dynamically remove required while it's hidden — browsers do not exempt hidden elements from constraint validation automatically. Form Studio handles this correctly for every multi-step and conditional form it generates.
Use the Free Tool
Form Studio applies the right native validation attribute for every field type automatically, and — critically for multi-step or conditionally-visible fields — keeps the required attribute in sync with actual visibility, so a required field on a later step or behind a conditional rule never silently blocks submission of the step you're currently on.
Native Validation Attributes
Cover these before reaching for JavaScript — they're part of the browser's built-in Constraint Validation API, work with zero code, are announced correctly by screen readers, and trigger the browser's built-in error UI automatically:
<input type="email" name="email" required> <input type="text" name="username" minlength="3" maxlength="20" required> <input type="number" name="age" min="18" max="120"> <input type="text" name="zip" pattern="[0-9]{5}" title="5-digit ZIP code"> <input type="url" name="website">Submitting a form with any of these unmet triggers the browser's native inline error bubble and blocks the request from being sent — no JavaScript required.
Triggering Validation Programmatically
Two methods matter for anything beyond a plain form submit:
form.checkValidity() returns true/false without showing any UI — useful for a "can I proceed" check before doing something else, like advancing a multi-step form to the next step.
form.reportValidity() does the same check and shows the browser's native error bubbles on the first invalid field, exactly as if the user had clicked submit. This is what you want for a "Next" button on a multi-step form:
nextButton.addEventListener('click', () => { if (!form.reportValidity()) return; // shows errors on the current step, stops here showStep(currentStep + 1); });Custom Validation Messages
The browser's default messages ("Please fill out this field") are generic. setCustomValidity() overrides them with your own text, and clearing it back to an empty string re-enables normal validation:
const passwordInput = document.getElementById('password'); const confirmInput = document.getElementById('confirm-password'); confirmInput.addEventListener('input', () => { if (confirmInput.value !== passwordInput.value) { confirmInput.setCustomValidity('Passwords must match'); } else { confirmInput.setCustomValidity(''); // clear it — this is easy to forget and leaves the field permanently invalid } });The most common bug with setCustomValidity is forgetting the else branch — once you set a custom message, the field stays invalid on every future check until you explicitly clear it, even after the actual problem is fixed.
The Hidden-Field Validation Trap
Here's the part that catches almost everyone building a multi-step or conditionally-visible form: browsers do not exempt an element from constraint validation just because an ancestor has the hidden attribute.
Concretely — a two-step form where step 2 has a required field, and step 2 is hidden with hidden while the user is on step 1:
<div class="step-one"> <input type="text" name="name" required> </div> <div class="step-two" hidden> <input type="email" name="email" required> </div>This looks like it should be fine — the email field is invisible, so surely it doesn't block anything yet. It does. form.reportValidity() (or a native submit) checks every required field in the form regardless of visibility, and a hidden-but-required field fails that check silently — the "Next" button (if wired to reportValidity()) simply does nothing, with no visible error on the field the user can actually see, because the invalid field is the hidden one.
The fix is to dynamically toggle required based on actual visibility, restoring it when the field becomes visible again:
function syncRequired() { const controls = form.querySelectorAll('[required], [data-required]'); controls.forEach((control) => { // remember which controls were originally required, the first time we see them if (control.hasAttribute('required') && !control.hasAttribute('data-required')) { control.setAttribute('data-required', ''); } if (control.hasAttribute('data-required')) { if (control.closest('[hidden]')) { control.removeAttribute('required'); } else { control.setAttribute('required', ''); } } }); }Call syncRequired() every time visibility changes — after switching steps, and after re-evaluating conditional-visibility rules — so required always reflects what's actually on screen. This exact pattern is what Form Studio generates for every multi-step and conditional-logic form, across every code export format, precisely because this bug is so easy to ship without ever noticing in manual testing (it only shows up once a form has both a later step or conditional section and a required field inside it).
Step-by-Step Workflow
Step 1: Add the correct native attribute for each field's validation needs — required, type, minlength/maxlength, min/max, pattern — before writing any JavaScript.
Step 2: If the form is single-step with no conditional visibility, native validation alone is sufficient — the browser blocks submission and shows errors automatically.
Step 3: If the form has multiple steps or conditionally-hidden fields, wire "Next" buttons to form.reportValidity() and implement the syncRequired() pattern above so hidden-but-required fields never block the steps you can see.
Step 4: Add setCustomValidity() only for checks native attributes can't express (cross-field comparisons like password confirmation, checks against external data).
Step 5: Re-validate everything server-side regardless of what the client checked — client-side validation is a UX feature, not a security boundary.
Common Mistakes
Shipping a multi-step or conditional form without the hidden-field fix. The bug is silent — no console error, no visible failure — until a user with a required field on a later or hidden section gets stuck with no explanation.
Forgetting to clear setCustomValidity(''). A field stays permanently invalid once a custom message is set, even after the underlying issue is resolved.
Relying on pattern for anything beyond simple format checks. Complex validation logic (cross-field comparisons, checks requiring a server round-trip) belongs in JavaScript, not a regular expression crammed into an attribute.
Only testing the happy path. The hidden-field bug specifically only appears once you test a required field that's actually behind a hidden step or a conditional rule — a form tested only in its default, fully-visible state won't reveal it.
Trusting client-side validation as sufficient on its own. Anyone can submit directly to your endpoint bypassing the browser entirely — always re-validate server-side.
Best Practices
Prefer native attributes over custom JavaScript validation wherever they can express the rule — less code, correct accessibility announcements, and consistent browser UI for free.
Test every required field in its hidden state, not just its visible one, for any form with steps or conditional logic.
Keep a single
syncRequired()-style function rather than scatteringrequired/removeAttribute('required')calls across multiple event handlers — one source of truth is much easier to keep correct as the form grows.Always re-validate server-side — treat client-side checks as a UX convenience only.
Use
titlealongsidepatternto give the browser's default error message something more specific to show than a generic format complaint.
Related Tools
Form Studio — Build a form with correct validation, including the hidden-field fix for multi-step and conditional forms
HTML Text Input Generator — Generate a single input with the right validation attributes
See also How to Secure a Public Form Endpoint for the server-side half of validation, and the Developer Tools category for the rest of the form utilities.
FAQ
Does required work on a <select> element?
Yes — a <select> with required and no non-empty option selected (typically achieved with a disabled, value-less first <option>) fails validation exactly like a text input would.
Can I validate a radio group or checkbox group as a whole?
Native validation checks each individual radio/checkbox input, not the group as a concept — but marking required on just the first radio in a group is sufficient, since the browser only requires that some option in a same-named radio group be checked, not every one of them.
What's the difference between checkValidity() and reportValidity()?
checkValidity() returns a boolean silently. reportValidity() does the same check but also displays the browser's native error UI on the first invalid field — use it wherever you want the user to actually see what's wrong, like a "Next" button on a multi-step form.
Why does my hidden required field break the "Next" button but show no visible error?
Because the invalid field is the hidden one — reportValidity() tries to focus and show an error bubble on it, but a field with display: none or hidden can't be focused or shown, so the browser silently fails the check with nothing visible. This is exactly the trap described above; the fix is removing required while the field is actually hidden.
Is client-side validation ever unnecessary?
No — even though it's not a security boundary, it meaningfully improves the experience by catching mistakes instantly, without a round-trip to your server. Skip it only for very low-stakes internal tools where the extra UX polish genuinely doesn't matter.
Validate Your Form Correctly Now
Form Studio applies the right native validation attributes automatically, and handles the hidden-field trap correctly for every multi-step and conditional-logic form it generates. Free, and it runs entirely in your browser.
Khushbu
Full-Stack Developer & Founder
I build tools I wish existed — fast, free, and private. Every tool runs in your browser because I believe your data should stay yours.
Put this guide into practice

