Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

The Form Validation API You Already Shipped

C
Carlo Fontanos
· 2 min read

Between "just use the required attribute" and "npm install a validation library" sits a layer most developers never explore: the browser's built-in validation engine has a complete JavaScript API, and it does 90% of what the libraries do.

The attributes are the rules; the API is the control

<form id="signup" novalidate>
    <input name="username" required minlength="3" pattern="[a-z0-9_]+">
    <input name="email" type="email" required>
</form>

novalidate is the counterintuitive key move: it disables the browser's default bubbles and timing while keeping all the validation state - so you decide when and how errors show:

form.addEventListener('submit', (e) => {
    if (!form.checkValidity()) {
        e.preventDefault();
        for (const field of form.elements) {
            showError(field);   // your markup, your styling, your timing
        }
        form.querySelector(':invalid')?.focus();
    }
});

function showError(field) {
    const box = field.parentElement.querySelector('.error');
    if (box) box.textContent = field.validationMessage;   // localized, human, free
}

field.validationMessage is the browser's own message - "Please fill out this field", in the user's language, for every rule. You get i18n'd error copy without writing any.

validity: know exactly what's wrong

field.validity.valueMissing;     // required but empty
field.validity.patternMismatch;  // failed the regex
field.validity.tooShort;         // under minlength
field.validity.typeMismatch;     // not a valid email/url

Branch on these for custom copy per failure ("Usernames are lowercase letters, numbers and _") while keeping the engine's logic. Pair with the :user-invalid CSS pseudo-class (or :has() on the wrapper) so styling needs no JS at all.

setCustomValidity: plug in your own rules

Anything the attributes can't express - including async checks - joins the same system:

username.addEventListener('blur', async () => {
    username.setCustomValidity('');                       // clear first - always
    if (!username.validity.valid) return;                 // built-in rules first

    const taken = await isUsernameTaken(username.value);
    if (taken) username.setCustomValidity('That username is taken.');
    username.reportValidity();                            // show it now
});

A non-empty custom validity makes the field invalid to everything - checkValidity, :invalid, form submission. The one trap: forgetting to reset with setCustomValidity('') leaves the field permanently invalid; clear-then-check, every time.

Password-confirmation matching, "date must be in the future", inventory checks - they all fit this shape. The library becomes worth it when you need schema-driven forms or cross-field dependency graphs; until then, the platform's engine plus thirty lines of glue is smaller, faster and already speaks 100 languages.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading