HTML forms with validation and accessibility

Alex Chang Feb 2026
1 tab
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>
    .error { color: red; font-size: 0.875rem; }
    input:invalid { border-color: red; }
    input:valid { border-color: green; }
    .required::after { content: " *"; color: red; }
  </style>
</head>
<body>
  <form id="signup-form" novalidate>
    <!-- Text input with validation -->
    <div class="form-group">
      <label for="username" class="required">Username</label>
      <input
        type="text"
        id="username"
        name="username"
        required
        minlength="3"
        maxlength="20"
        pattern="[a-zA-Z0-9_]+"
        aria-describedby="username-hint username-error"
        autocomplete="username"
      >
      <small id="username-hint">3-20 characters, letters, numbers, underscore only</small>
      <span id="username-error" class="error" role="alert"></span>
    </div>

    <!-- Email input -->
    <div class="form-group">
      <label for="email" class="required">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        required
        aria-describedby="email-error"
        autocomplete="email"
      >
      <span id="email-error" class="error" role="alert"></span>
    </div>

    <!-- Password with requirements -->
    <div class="form-group">
      <label for="password" class="required">Password</label>
      <input
        type="password"
        id="password"
        name="password"
        required
        minlength="8"
        pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
        aria-describedby="password-hint password-error"
        autocomplete="new-password"
      >
      <small id="password-hint">
        Must contain at least 8 characters, one uppercase, one lowercase, and one number
      </small>
      <span id="password-error" class="error" role="alert"></span>
    </div>

    <!-- Phone number -->
    <div class="form-group">
      <label for="phone">Phone Number</label>
      <input
        type="tel"
        id="phone"
        name="phone"
        pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
        placeholder="123-456-7890"
        aria-describedby="phone-hint"
        autocomplete="tel"
      >
      <small id="phone-hint">Format: 123-456-7890</small>
    </div>

    <!-- Date of birth -->
    <div class="form-group">
      <label for="dob" class="required">Date of Birth</label>
      <input
        type="date"
        id="dob"
        name="dob"
        required
        min="1920-01-01"
        max="2008-12-31"
        aria-describedby="dob-error"
        autocomplete="bday"
      >
      <span id="dob-error" class="error" role="alert"></span>
    </div>

    <!-- Number input with range -->
    <div class="form-group">
      <label for="age" class="required">Age</label>
      <input
        type="number"
        id="age"
        name="age"
        required
        min="18"
        max="120"
        aria-describedby="age-error"
      >
      <span id="age-error" class="error" role="alert"></span>
    </div>

    <!-- URL input -->
    <div class="form-group">
      <label for="website">Website</label>
      <input
        type="url"
        id="website"
        name="website"
        placeholder="https://example.com"
        autocomplete="url"
      >
    </div>

    <!-- Select dropdown -->
    <div class="form-group">
      <label for="country" class="required">Country</label>
      <select id="country" name="country" required aria-describedby="country-error">
        <option value="">Select a country</option>
        <option value="us">United States</option>
        <option value="ca">Canada</option>
        <option value="uk">United Kingdom</option>
      </select>
      <span id="country-error" class="error" role="alert"></span>
    </div>

    <!-- Radio buttons -->
    <fieldset>
      <legend class="required">Gender</legend>
      <div>
        <input type="radio" id="male" name="gender" value="male" required>
        <label for="male">Male</label>
      </div>
      <div>
        <input type="radio" id="female" name="gender" value="female">
        <label for="female">Female</label>
      </div>
      <div>
        <input type="radio" id="other" name="gender" value="other">
        <label for="other">Other</label>
      </div>
      <span id="gender-error" class="error" role="alert"></span>
    </fieldset>

    <!-- Checkboxes -->
    <fieldset>
      <legend>Interests (select all that apply)</legend>
      <div>
        <input type="checkbox" id="interest-coding" name="interests" value="coding">
        <label for="interest-coding">Coding</label>
      </div>
      <div>
        <input type="checkbox" id="interest-design" name="interests" value="design">
        <label for="interest-design">Design</label>
      </div>
      <div>
        <input type="checkbox" id="interest-writing" name="interests" value="writing">
        <label for="interest-writing">Writing</label>
      </div>
    </fieldset>

    <!-- Textarea -->
    <div class="form-group">
      <label for="bio">Bio</label>
      <textarea
        id="bio"
        name="bio"
        rows="4"
        maxlength="500"
        aria-describedby="bio-hint"
      ></textarea>
      <small id="bio-hint">Maximum 500 characters</small>
    </div>

    <!-- Terms acceptance -->
    <div class="form-group">
      <input
        type="checkbox"
        id="terms"
        name="terms"
        required
        aria-describedby="terms-error"
      >
      <label for="terms" class="required">
        I agree to the <a href="/terms">Terms and Conditions</a>
      </label>
      <span id="terms-error" class="error" role="alert"></span>
    </div>

    <!-- File upload -->
    <div class="form-group">
      <label for="avatar">Profile Picture</label>
      <input
        type="file"
        id="avatar"
        name="avatar"
        accept="image/png, image/jpeg"
        aria-describedby="avatar-hint"
      >
      <small id="avatar-hint">PNG or JPEG, max 2MB</small>
    </div>

    <!-- Submit button -->
    <button type="submit">Sign Up</button>
    <button type="reset">Clear Form</button>
  </form>

  <script>
    const form = document.getElementById('signup-form');

    form.addEventListener('submit', function(e) {
      e.preventDefault();

      // Clear previous errors
      document.querySelectorAll('.error').forEach(el => el.textContent = '');
      document.querySelectorAll('input, select').forEach(el => {
        el.setAttribute('aria-invalid', 'false');
      });

      let isValid = true;

      // Validate each field
      const fields = form.querySelectorAll('input[required], select[required]');
      fields.forEach(field => {
        if (!field.validity.valid) {
          isValid = false;
          const errorElement = document.getElementById(field.id + '-error');

          if (field.validity.valueMissing) {
            errorElement.textContent = 'This field is required';
          } else if (field.validity.typeMismatch) {
            errorElement.textContent = 'Please enter a valid ' + field.type;
          } else if (field.validity.tooShort) {
            errorElement.textContent = `Minimum length is ${field.minLength} characters`;
          } else if (field.validity.tooLong) {
            errorElement.textContent = `Maximum length is ${field.maxLength} characters`;
          } else if (field.validity.rangeUnderflow) {
            errorElement.textContent = `Minimum value is ${field.min}`;
          } else if (field.validity.rangeOverflow) {
            errorElement.textContent = `Maximum value is ${field.max}`;
          } else if (field.validity.patternMismatch) {
            errorElement.textContent = 'Please match the required format';
          }

          field.setAttribute('aria-invalid', 'true');
          field.focus();
        }
      });

      if (isValid) {
        console.log('Form is valid!', new FormData(form));
        // Submit form
      }
    });

    // Real-time validation
    form.querySelectorAll('input, select').forEach(field => {
      field.addEventListener('blur', function() {
        if (this.value && !this.validity.valid) {
          const errorElement = document.getElementById(this.id + '-error');
          if (errorElement) {
            errorElement.textContent = 'Invalid input';
            this.setAttribute('aria-invalid', 'true');
          }
        }
      });

      field.addEventListener('input', function() {
        if (this.validity.valid) {
          const errorElement = document.getElementById(this.id + '-error');
          if (errorElement) {
            errorElement.textContent = '';
            this.setAttribute('aria-invalid', 'false');
          }
        }
      });
    });
  </script>
</body>
</html>
1 file · html Explain with highlit

HTML5 form validation uses attributes like required, pattern, min, max, and type to validate input. I leverage native validation before JavaScript. The <label> element associates text with inputs for accessibility. Input types like email, tel, url, and date provide built-in validation and mobile keyboards. The autocomplete attribute helps users fill forms faster. The novalidate attribute allows custom validation. Using aria-invalid and aria-describedby announces errors to screen readers. Fieldsets group related inputs with <legend> for context. Proper form design improves user experience and conversion rates.