Native HTML Form Validation Enhancer with Zod Support – validation-enhancer

Category: Form , Javascript | June 12, 2026
Authoralistairldavidson
Last UpdateJune 12, 2026
LicenseMIT
Views0 views
Native HTML Form Validation Enhancer with Zod Support – validation-enhancer

validation-enhancer is a JavaScript web component that upgrades native HTML5 form validation with the inline error messages, CSS state classes, and accessibility behavior that the browser’s built-in validation never provides on its own.

Wrap any <form> in the <validation-enhancer> custom element and you get per-field error text, .valid and .invalid class toggling, aria-invalid management, and a scroll-to-first-error behavior on submit.

A companion element, <validation-enhancer-zod>, replaces the HTML attribute checks with a Zod schema, so a single z.object() definition drives all field rules and error messages.

Features:

  • Enhances native HTML form validation inside a custom element.
  • Displays validation messages next to each form field.
  • Shows errors after focus leaves a field or after form submit.
  • Clears validation text after the user fixes a field.
  • Focuses the first invalid field on form submit.
  • Scrolls the first invalid field or its label into view.
  • Supports custom validation text through HTML attributes.
  • Supports Zod schemas for schema-first validation.
  • Watches newly added forms and fields after page load.
  • Adds field state classes for custom CSS styling.

How To Use It:

Installation

1. Install & import validation-enhancer.

npm install validation-enhancer
import "validation-enhancer";

2. Import the Zod version only when you need schema validation.

import "validation-enhancer-zod";

3. For a browser or demo use, copy the compiled file from the dist directory into your project and load it as a module.

<script type="module" src="validation-enhancer.min.js"></script>

4. Or use the Zod build for schema-based validation.

<script type="module" src="validation-enhancer-zod.min.js"></script>

5. Available builds:

dist/validation-enhancer.js
dist/validation-enhancer.min.js
dist/validation-enhancer.compat.min.js
dist/validation-enhancer-zod.js
dist/validation-enhancer-zod.min.js
dist/validation-enhancer-zod.compat.min.js

The compat builds target ES2017 output. The Zod builds include the main validation enhancer component.

The required HTML markup

Wrap the form in <validation-enhancer>. Add an error message element for each field and connect it with aria-errormessage.

<validation-enhancer>
  <form id="newsletter-form" action="/newsletter" method="post">
    <label for="subscriber-email">Email address</label>
    <input
      id="subscriber-email"
      name="email"
      type="email"
      required
      aria-errormessage="subscriber-email-error"
    >
    <div id="subscriber-email-error" class="field-error"></div>
    <button type="submit">Subscribe</button>
  </form>
</validation-enhancer>

Basic Usage

The smallest useful setup needs the wrapper component, a form, at least one validatable input, a message target, a small amount of CSS, and the module script.

<validation-enhancer>
  <form id="contact-form" action="/contact" method="post">
    <label for="contact-email">Email address</label>
    <input
      id="contact-email"
      name="email"
      type="email"
      required
      aria-errormessage="contact-email-error"
      validation-valueMissing="Enter your email address."
      validation-typeMismatch="Enter a valid email address."
    >
    <div id="contact-email-error" class="field-error"></div>
    <button type="submit">Send message</button>
  </form>
</validation-enhancer>
<style>
input.valid {
  border-color: #2e7d32;
}
input.invalid {
  border-color: #c62828;
}
.field-error {
  color: #c62828;
  font-size: 0.875rem;
  margin-top: 0.35rem;
}
</style>
<script type="module" src="validation-enhancer.min.js"></script>

The component disables the browser’s default validation popup for wrapped forms. It checks each input through the standard checkValidity() API. It writes the validation message into the linked error element, updates field classes, and sets aria-invalid="true" on invalid fields.

Configuration Options

  • valid-class (String): Sets the class added to fields that pass validation. Default value is valid.
  • invalid-class (String): Sets the class added to fields that fail validation. Default value is invalid.
  • message-target-attr (String): Set the attribute used to find each field’s message element. Default value is aria-errormessage.
  • message-prefix (String): Sets the prefix for custom validation message attributes. Default value is validation.
  • message-aria-live (String): Sets the aria-live value added to message elements. Default value is polite.
  • scroll-behavior (String): Sets the behavior option passed to scrollIntoView(). Default value is smooth.
  • scroll-block (String): Sets the block option passed to scrollIntoView().
  • scroll-inline (String): Sets the inline option passed to scrollIntoView().
  • scroll-align-to-top (Boolean-like String): Uses the boolean form of scrollIntoView() when present. Use false to pass false; any other value passes true.
  • scroll-container (String): Sets a CSS selector for a custom scroll container. The component scrolls that container instead of the document.

Custom validation message attributes follow browser validity keys.

<input
  id="signup-email"
  name="email"
  type="email"
  required
  aria-errormessage="signup-email-error"
  validation-valueMissing="Email is required."
  validation-typeMismatch="Use a valid email address."
>
<div id="signup-email-error"></div>

The component also accepts the data- form.

<input
  required
  aria-errormessage="name-error"
  data-validation-valueMissing="Enter your name."
>
<div id="name-error"></div>

Supported validity keys:

  • validation-valueMissing (String): Message for an empty required field.
  • validation-typeMismatch (String): Message for a value that does not match the input type.
  • validation-patternMismatch (String): Message for a value that does not match the input pattern.
  • validation-tooLong (String): Message for a value that exceeds maxlength.
  • validation-tooShort (String): Message for a value shorter than minlength.
  • validation-rangeUnderflow (String): Message for a number below min.
  • validation-rangeOverflow (String): Message for a number above max.
  • validation-stepMismatch (String): Message for a number that does not match step.
  • validation-badInput (String): Message for a value the browser cannot parse.

API Methods

// Set a Zod schema on a validation-enhancer-zod element.
// Input names must match schema field names.
const enhancedForm = document.querySelector("validation-enhancer-zod");
enhancedForm.setZodSchema(schema);
// Read the current Zod schema from a validation-enhancer-zod element.
const currentSchema = enhancedForm.getZodSchema();
// Set a custom validity message on a native form field.
// This uses the standard HTMLFormElement API.
const emailInput = document.querySelector("#account-email");
emailInput.setCustomValidity("This email address is already in use.");
// Clear a custom validity message.
emailInput.setCustomValidity("");
// Trigger validation after a script changes a field value.
emailInput.value = "[email protected]";
emailInput.dispatchEvent(new Event("change", { bubbles: true }));

Events and Integration Hooks

validation-enhancer does not expose official custom events. It listens to standard DOM events inside the wrapped component.

const form = document.querySelector("#profile-form");
// Integration hook: run custom code before a form leaves the page.
form.addEventListener("submit", (event) => {
  if (!form.checkValidity()) {
    return;
  }
  console.log("The form passed native validation.");
});
// Integration hook: react when a field value changes.
form.addEventListener("change", (event) => {
  if (event.target.matches("input, select, textarea")) {
    console.log("Changed field:", event.target.name);
  }
});
// Integration hook: trigger validation after programmatic value changes.
const username = document.querySelector("#profile-username");
username.value = "new-editor";
username.dispatchEvent(new Event("change", { bubbles: true }));

Advanced Example 1: Contact Form with Custom Messages

<validation-enhancer>
  <form id="support-form" action="/support" method="post">
    <label for="support-name">Name</label>
    <input
      id="support-name"
      name="name"
      required
      minlength="2"
      aria-errormessage="support-name-error"
      validation-valueMissing="Enter your name."
      validation-tooShort="Use at least 2 characters."
    >
    <div id="support-name-error" class="form-error"></div>
    <label for="support-email">Email</label>
    <input
      id="support-email"
      name="email"
      type="email"
      required
      aria-errormessage="support-email-error"
      validation-valueMissing="Enter your email address."
      validation-typeMismatch="Use a valid email format."
    >
    <div id="support-email-error" class="form-error"></div>
    <label for="support-message">Message</label>
    <textarea
      id="support-message"
      name="message"
      required
      minlength="20"
      aria-errormessage="support-message-error"
      validation-valueMissing="Enter a support message."
      validation-tooShort="Use at least 20 characters."
    ></textarea>
    <div id="support-message-error" class="form-error"></div>
    <button type="submit">Send request</button>
  </form>
</validation-enhancer>
<script type="module" src="validation-enhancer.min.js"></script>

Advanced Example 2: Custom Class Names and Message Prefix

<validation-enhancer
  valid-class="is-valid"
  invalid-class="has-error"
  message-prefix="form"
>
  <form id="billing-form" action="/billing" method="post">
    <label for="billing-postcode">ZIP code</label>
    <input
      id="billing-postcode"
      name="postcode"
      required
      pattern="[0-9]{5}"
      aria-errormessage="billing-postcode-error"
      form-valueMissing="Enter a ZIP code."
      form-patternMismatch="Use a 5 digit ZIP code."
    >
    <div id="billing-postcode-error" class="field-message"></div>
    <button type="submit">Save billing address</button>
  </form>
</validation-enhancer>
<style>
input.is-valid {
  border-color: #2e7d32;
}
input.has-error {
  border-color: #b00020;
}
.field-message {
  color: #b00020;
}
</style>
<script type="module" src="validation-enhancer.min.js"></script>

Advanced Example 3: Zod Schema Validation

<validation-enhancer-zod>
  <form id="account-form" action="/account" method="post">
    <label for="account-email">Email</label>
    <input
      id="account-email"
      name="email"
      type="email"
      aria-errormessage="account-email-error"
    >
    <div id="account-email-error" class="form-error"></div>
    <label for="account-password">Password</label>
    <input
      id="account-password"
      name="password"
      type="password"
      aria-errormessage="account-password-error"
    >
    <div id="account-password-error" class="form-error"></div>
    <button type="submit">Create account</button>
  </form>
</validation-enhancer-zod>
<script type="module">
  import { z } from "https://esm.sh/zod";
  import "validation-enhancer-zod";
  const schema = z.object({
    email: z.string()
      .min(1, "Email is required.")
      .email("Enter a valid email address."),
    password: z.string()
      .min(8, "Use at least 8 characters.")
  });
  customElements.whenDefined("validation-enhancer-zod").then(() => {
    const formEnhancer = document.querySelector("validation-enhancer-zod");
    formEnhancer.setZodSchema(schema);
  });
</script>

Zod validation matches fields by name. A field with no name attribute does not map to a schema field.

Advanced Example 4: Dynamic Fields After Page Load

<validation-enhancer>
  <form id="team-form" action="/team" method="post">
    <div id="member-list">
      <label for="member-1">Team member email</label>
      <input
        id="member-1"
        name="members[]"
        type="email"
        required
        aria-errormessage="member-1-error"
        validation-valueMissing="Enter an email address."
        validation-typeMismatch="Use a valid email address."
      >
      <div id="member-1-error" class="field-error"></div>
    </div>
    <button type="button" id="add-member">Add member</button>
    <button type="submit">Invite team</button>
  </form>
</validation-enhancer>
<script type="module" src="validation-enhancer.min.js"></script>
<script>
  let memberCount = 1;
  document.querySelector("#add-member").addEventListener("click", () => {
    memberCount += 1;
    const fieldId = `member-${memberCount}`;
    const errorId = `${fieldId}-error`;
    document.querySelector("#member-list").insertAdjacentHTML("beforeend", `
      <label for="${fieldId}">Team member email</label>
      <input
        id="${fieldId}"
        name="members[]"
        type="email"
        required
        aria-errormessage="${errorId}"
        validation-valueMissing="Enter an email address."
        validation-typeMismatch="Use a valid email address."
      >
      <div id="${errorId}" class="field-error"></div>
    `);
  });
</script>

The component watches added elements inside the wrapper. New inputs and message targets join the validation flow after insertion.

Alternatives and Related Resources:

You Might Be Interested In:


Leave a Reply