DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

HTML5 Constraint Validation API: setCustomValidity, checkValidity, Real-Time Validation, Accessibility

Browser-native form validation is underused by most web developers. Firefox implements the HTML5 constraint validation API fully, which means built-in validation for required fields, email patterns, number ranges, and custom patterns — without JavaScript. And when you need custom validation, the Constraint Validation API gives you complete control over error messages and UI.

Built-In Validation Attributes

HTML5 validation attributes work without any JavaScript:

<form>
  <!-- Required field -->
  <input type="text" required placeholder="Name">

  <!-- Email validation -->
  <input type="email" required>

  <!-- Number range -->
  <input type="number" min="1" max="100" step="5">

  <!-- URL validation -->
  <input type="url" placeholder="https://example.com">

  <!-- Pattern matching -->
  <input type="text" pattern="[A-Za-z]{3,10}" title="3-10 letters only">

  <!-- String length -->
  <input type="text" minlength="3" maxlength="50">

  <!-- File type restriction -->
  <input type="file" accept=".pdf,.docx">
</form>
Enter fullscreen mode Exit fullscreen mode

When the form is submitted, Firefox validates all fields and prevents submission if any fail. It shows native validation UI (red outline + tooltip with the error message).

Type-Based Validation

Setting type correctly adds validation automatically:

Type Validates
email Valid email format
url Valid URL format
number Numeric value, respects min/max/step
date Valid date in browser's calendar format
time Valid time format
color Valid hex color
tel No validation (phone formats vary globally)
search No validation (same as text)

Custom Error Messages

Native error messages are browser-localized but not always helpful. Customize them:

const emailInput = document.getElementById('email');

emailInput.addEventListener('input', () => {
  if (emailInput.validity.typeMismatch) {
    emailInput.setCustomValidity('Please enter a valid email address');
  } else {
    emailInput.setCustomValidity(''); // Clear custom message
  }
});
Enter fullscreen mode Exit fullscreen mode

Important: setCustomValidity('') clears the custom message. If you don't clear it, the field stays invalid even when the user types a valid value.

The Constraint Validation API

Every form element has validation properties:

const input = document.getElementById('username');

// Check validity
input.validity.valid          // true if all constraints pass
input.validity.valueMissing   // true if required and empty
input.validity.typeMismatch   // true if type doesn't match (e.g., non-email in email field)
input.validity.patternMismatch // true if pattern doesn't match
input.validity.tooShort       // true if below minlength
input.validity.tooLong        // true if above maxlength
input.validity.rangeUnderflow // true if below min
input.validity.rangeOverflow  // true if above max
input.validity.stepMismatch   // true if value not on step intervals
input.validity.customError    // true if setCustomValidity() set a message

// Get the current validity message
input.validationMessage       // the error message (localized or custom)

// Programmatically check and report
input.checkValidity()         // returns true/false, fires 'invalid' event on failure
input.reportValidity()        // same but also shows the native error bubble
Enter fullscreen mode Exit fullscreen mode

Preventing Native Validation UI

To use the API but implement your own error UI, add novalidate to the form and handle validation manually:

const form = document.getElementById('myForm');
form.setAttribute('novalidate', ''); // Disable native UI

form.addEventListener('submit', (event) => {
  event.preventDefault();

  let hasErrors = false;

  form.querySelectorAll('input, select, textarea').forEach(field => {
    if (!field.checkValidity()) {
      hasErrors = true;
      showError(field, field.validationMessage);
    } else {
      clearError(field);
    }
  });

  if (!hasErrors) {
    submitForm();
  }
});

function showError(field, message) {
  const errorEl = field.nextElementSibling;
  if (errorEl?.classList.contains('error')) {
    errorEl.textContent = message;
    field.setAttribute('aria-invalid', 'true');
  }
}
Enter fullscreen mode Exit fullscreen mode

Real-Time Validation

For validation on every keystroke (after the user first touches the field):

function validateField(field) {
  field.addEventListener('blur', () => {
    field.dataset.touched = 'true';
    checkField(field);
  });

  field.addEventListener('input', () => {
    if (field.dataset.touched) {
      checkField(field);
    }
  });
}

function checkField(field) {
  if (!field.checkValidity()) {
    showError(field, field.validationMessage);
  } else {
    clearError(field);
  }
}
Enter fullscreen mode Exit fullscreen mode

This pattern validates after the user leaves a field (blur), then on every keystroke until it's valid.

Custom Validation Logic

For validation that goes beyond HTML5 attributes:

const passwordField = document.getElementById('password');
const confirmField = document.getElementById('confirm-password');

confirmField.addEventListener('input', () => {
  if (confirmField.value !== passwordField.value) {
    confirmField.setCustomValidity("Passwords don't match");
  } else {
    confirmField.setCustomValidity('');
  }
});

// Async validation (checking username availability)
let debounceTimer;
usernameField.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(async () => {
    const response = await fetch(`/api/check-username?u=${usernameField.value}`);
    const { available } = await response.json();

    if (!available) {
      usernameField.setCustomValidity('Username already taken');
    } else {
      usernameField.setCustomValidity('');
    }
    usernameField.reportValidity();
  }, 500);
});
Enter fullscreen mode Exit fullscreen mode

Accessibility with Form Validation

Native validation doesn't always work well with screen readers. For accessible custom validation:

<!-- Associate error message with input via aria-describedby -->
<div>
  <label for="email">Email</label>
  <input type="email" id="email" aria-describedby="email-error" aria-invalid="true">
  <span id="email-error" role="alert">Please enter a valid email address</span>
</div>
Enter fullscreen mode Exit fullscreen mode
// Set aria-invalid dynamically
field.setAttribute('aria-invalid', field.validity.valid ? 'false' : 'true');
Enter fullscreen mode Exit fullscreen mode

The role="alert" on the error span makes screen readers announce it when it appears.

Firefox-Specific Behavior

Firefox's built-in validation tooltip position adapts to the element's position on screen. The tooltip appears above the field if the field is near the bottom of the viewport, otherwise below.

Firefox localizes validation messages to the user's browser language, not the page language. If you need consistent English messages, use setCustomValidity() to override them.

Testing Form Validation in Firefox

To test validation without actually submitting:

// Test each field's validity state
document.querySelectorAll('input, select, textarea').forEach(field => {
  console.log(field.name, {
    valid: field.validity.valid,
    message: field.validationMessage
  });
});

// Force display of all native validation errors:
document.getElementById('form').reportValidity();
Enter fullscreen mode Exit fullscreen mode

Top comments (0)