Native HTML form validation messages customizing: a build guide

Customize native HTML form validation messages without breaking browser behavior: read validity flags, use setCustomValidity, place errors inline, then test.

James HollowayWeb Development Lead9 min read

Most contact forms already have working validation. You added required, maybe type="email", and the browser refuses to submit until the visitor fixes things. What usually needs work is the wording: the browser's built-in copy is terse, sometimes cryptic, and it may not match the tone of the rest of your site.

The good news is that native HTML form validation messages customizing does not require a validation library or a rebuilt form. You enhance the constraints you already have. This guide walks through reading the Constraint Validation API, replacing messages with setCustomValidity, deciding between native bubbles and inline errors, and running a test script before launch. It also covers how the Detected Form widget layers onto your existing markup.

Start with what the platform gives you before writing any JavaScript.

What native constraint validation already does

The browser validates form controls against attributes you declare in HTML: required, type, pattern, minlength, maxlength, min, max and step. If a value breaks a constraint, the form will not submit and the browser blocks it with an error message. MDN's guide to client-side form validation is explicit that client-side validation is an initial check for user experience, and that you should still validate on the server because client-side checks are easy to bypass.

That last point matters for your scope. You are improving the client-side experience, not replacing server-side safety. Your form's destination or handler should still check whatever arrives.

Practically, most contact forms need fewer custom rules than people assume. A name field with required, an email field with type="email", a message textarea with required and a reasonable maxlength, plus a consent checkbox, covers the common cases. Before writing JavaScript, audit which attributes already exist and which fields genuinely need a format rule. Fewer constraints mean fewer error paths to test.

Reading the Constraint Validation API before you write copy

Every form control exposes a validity object, a validationMessage string and a checkValidity() method. The validity object has boolean flags: valueMissing for empty required fields, typeMismatch for a broken email or URL, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow and customError when you have set your own message.

Before customizing anything, log the state. In the console, select a field and inspect field.validity. Submit the form empty, then with a partial email, and watch which flags flip. This tells you which message a visitor actually sees for each mistake, so you rewrite the message that fires rather than a message you imagine fires.

Also note that validationMessage is read-only. You cannot assign to it. You influence it through constraints and through setCustomValidity.

A useful side detail: styling with :invalid and :user-invalid lets you highlight bad fields in CSS. MDN notes that :user-invalid applies after the user has interacted with the control, which avoids painting a fresh empty form red on first load. That CSS layer is often enough before touching any JavaScript.

Writing clearer messages with setCustomValidity

The mechanism is small. field.setCustomValidity('Enter a phone number like 07700 900123') sets a custom error and marks the field invalid. Calling field.setCustomValidity('') clears it. On submit, the browser shows your string in its normal bubble.

Two rules prevent the classic bugs:

  • Clear the message on input. If you set a custom message and never reset it, the field stays invalid even after the visitor types a valid value. Add an input listener that calls setCustomValidity(''), then re-check and re-set if needed.
  • Do not set custom messages on fields you do not control. Hidden fields, honeypots and consent inputs need separate handling, covered below.

A minimal pattern for a phone field:

<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel"
       pattern="[0-9 ]{9,15}" required>
const phone = document.getElementById('phone');

phone.addEventListener('input', () => {
  phone.setCustomValidity('');
});

phone.addEventListener('blur', () => {
  if (phone.validity.patternMismatch) {
    phone.setCustomValidity('Enter digits only, for example 07700 900123.');
  }
});

The wording rule is simple: say what is wrong and what to do next. "Invalid input" tells a visitor nothing. "Enter digits only, for example 07700 900123" gives them a model to copy.

If you want a fuller treatment of phrasing, the existing article on contact form error messages that help visitors fix and resubmit covers wording and inline placement in more depth, and the guide to HTML form validation for static site contact forms covers the constraint layer plus endpoint checks. This piece focuses on the customization mechanics and the widget hand-off.

Native bubbles, inline errors, or a form-level summary

You have three places to put an error, and mixing all three without a plan confuses people.

  • Native browser bubbles are free, localized by the browser, and appear next to the field. They are fine for short, obvious fixes and for low-stakes forms. Their limits: you cannot style them, you cannot add links or rich copy, and their placement varies by browser.
  • Inline error text below each field is the most controllable. You write every word, style it, and keep it visible while the visitor fixes the value. It requires markup and JavaScript to show and hide.
  • A form-level summary lists all errors near the submit button. It helps when many fields fail at once, but alone it forces scrolling back to find each field.

A workable default: use native bubbles for simple one-field misses on small forms, and move to inline text when the form has grouped fields, conditional fields, or any message longer than a short sentence. If you add a summary, pair it with inline text and move focus to the first invalid field so keyboard users are not stranded.

Whichever you choose, pick one primary channel per state. Native bubble plus inline text plus a summary for one empty field is noise.

Keeping error copy accessible

Errors that only appear in a colored border are invisible to screen reader users. The standard pattern ties the message to the input:

  • Give the error element an id and reference it from the input with aria-describedby.
  • Toggle aria-invalid="true" on the field while it is invalid, and remove it when valid.
  • Keep the label text unchanged. Do not replace a label with an error.
  • Move focus to the first invalid field on failed submit, so keyboard and screen reader users land where the fix is needed.
<label for="email">Email</label>
<input id="email" name="email" type="email" required
       aria-describedby="email-error" aria-invalid="false">
<p id="email-error" role="alert"></p>

If your form lives inside a modal or popup, focus management is part of the same job. The ARIA Authoring Practices dialog (modal) pattern describes moving focus into the dialog on open, containing Tab and Shift+Tab inside it, and returning focus to the trigger on close. When a submit fails inside that dialog, keep focus inside it too. The existing modal dialog accessibility test script gives you a repeatable keyboard pass for that case.

When several controls answer one question, wrap them in a fieldset with a legend. Radio groups, address blocks and consent sets all benefit. Screen readers announce the legend when focus enters the group, so a group error can be tied to the group rather than to one radio button.

Without a legend, a screen reader may announce "Yes, radio button" with no hint of what is being agreed to. With one, it says the question first. If you show a group-level error, put it inside the fieldset and reference it from the group container, not from a single control.

Where the Detected Form widget fits

The Detected Form widget connects an existing HTML form to WidgetJar. You create and configure the widget in the panel, then install the WidgetJar embed script on your registered website domain. It reads your existing form markup rather than generating a new form for you, which is why the constraint attributes and message work above still matter: the visitor still interacts with your inputs.

What you configure in the panel is the widget layer, not your field-level validation. Your required, pattern and setCustomValidity logic stay in your page. The widget does not rewrite your markup into a different form.

Because the widget sits on top of your form, test after installing the embed, not just before. Open the page with the script loaded and repeat the empty-submit and partial-submit passes. If you rely on autofill or unusual input types, the existing notes on the autocomplete off contact form fields decision explain which browsers ignore you and how to re-test after conversion.

Two fields usually need different treatment from the rest.

Consent checkboxes should be real validated controls. Use required on the checkbox, or a custom check with setCustomValidity on the field, and give it a clear label. Its error belongs next to the checkbox, not in a distant summary. The existing guide to adding a consent checkbox to an existing form covers how the widget handles consent records alongside your markup.

Honeypot fields must be hidden from real visitors and must not block legitimate submissions. A honeypot that is required, or one that your custom script marks invalid, will stop people who cannot see it. Leave honeypots out of your visible validation logic, hide them accessibly, and let the receiving side discard filled ones. The walkthrough on Detected Form widget honeypot spam protection covers timing and hidden-field setup.

A practical test script

Run these in order, once with a mouse and again with keyboard only, then once with a screen reader:

  1. Submit empty. Confirm each required field blocks submission, shows a readable message, and focus lands on the first invalid field.
  2. Submit partially. Fill one field correctly and leave another blank. Confirm the valid field is not flagged and the message names the field that still needs work.
  3. Enter invalid formats. Type a broken email, a short phone number, and an over-long message. Confirm each message tells the visitor what to do, not just that something is wrong.
  4. Correct and resubmit. Fix each field and confirm the error clears as you type or on blur, and that the form submits.
  5. Keyboard only. Tab through every field, trigger an error, and confirm you can reach and read the message without a mouse.
  6. Screen reader pass. Confirm the label, the error text and the invalid state are announced together, and that the focus move is not disorienting.
  7. After the embed. Repeat steps 1 and 4 with the WidgetJar script loaded on the live domain.

Record what you see in each browser you support. Bubble placement and wording differ between browsers, so a message that reads fine in one may wrap badly in another.

Pre-launch checklist

  • Every constraint you kept is one you actually need.
  • Every custom message says what to do, and clears on input.
  • One primary error channel per state; no triple reporting.
  • Errors are tied to inputs with aria-describedby and aria-invalid.
  • Focus moves to the first invalid field on failed submit.
  • Related controls sit in a fieldset with a legend.
  • Consent checkbox is validated and readable; honeypot is never required.
  • Server-side validation still runs on whatever the form posts, per the MDN guidance on sending and retrieving form data.
  • The destination still receives the corrected values after the embed is installed.

Put the improved form to work

You do not need to rebuild your contact form to get better messages. Tighten the constraints, read validity, write plain-language copy with setCustomValidity, and tie errors to inputs so everyone can find them. Then run the test script once more with the embed loaded.

When you are ready to connect that form to WidgetJar, configure it in the panel and install the script on your registered domain. Start from the Detected Form widget page to set up the layer on the form you already trust.

  • native HTML form validation messages customizing
  • forms
  • accessibility
  • validation
  • detected form

Written by

James Holloway

Web Development Lead at WidgetJar

Keep reading

Get started

Your next lead is one line away.

Pick a design, make it yours and paste a single script tag. Every widget type is included, and you do not need a credit card to start.

<script src="https://cdn.widgetjar.com/wj-assets/embed.js" defer></script>