<form id="loginForm" novalidate>
  <div>
    <label for="username">Username</label><br />
    <input type="text" id="username" name="username" />
  </div>
  <div>
    <label for="password">Password</label><br />
    <input type="password" id="password" name="password" />
  </div>
  <button type="submit">Log In</button>
</form>

<script>
  /**
   * Map of validation error keys to their spoken messages.
   * @type {{ [key: string]: string }}
   */
  const errorMessages = {
    usernameEmpty: "The Username field cannot be empty",
    passwordEmpty: "The Password field cannot be empty",
    // add more keys/messages here as needed...
  };
  /**
   * Speaks a given message using the Web Speech API.
   *
   * @param {string} message - The text to speak.
   * @param {string} [lang='en-US'] - The BCP-47 language code.
   */
  function speakError(message, lang = "en-US") {
    const utterance = new SpeechSynthesisUtterance(message);
    utterance.lang = lang;
    speechSynthesis.speak(utterance);
  }
  /**
   * Validates that the given input is not empty, styles it, and speaks an error if it is.
   *
   * @param {HTMLInputElement} element - The input element to validate.
   * @param {string} messageKey - The key identifying which error message to speak.
   */
  function validateNotEmpty(element, messageKey) {
    if (element.value.trim().length === 0) {
      element.style.border = "1px solid red";
      speakError(errorMessages[messageKey]);
    } else {
      element.style.border = "1px solid black";
    }
  }
  /**
   * @typedef {Object} FieldConfig
   * @property {string} id - The DOM id of the input field.
   * @property {string} messageKey - The key in errorMessages to use.
   */
  /** @type {FieldConfig[]} */
  const fields = [{
      id: "username",
      messageKey: "usernameEmpty"
    },
    {
      id: "password",
      messageKey: "passwordEmpty"
    },
  ];
  // Wire up blur listeners once DOM is ready
  fields.forEach(({
    id,
    messageKey
  }) => {
    const el = /** @type {HTMLInputElement|null} */ (document.getElementById(id));
    if (!el) return;
    el.addEventListener("blur", () => validateNotEmpty(el, messageKey));
  });
  // Prevent form submission for demo purposes
  const form = document.getElementById("loginForm");
  if (form) {
    form.addEventListener("submit", (e) => e.preventDefault());
  }
</script>
form {
  max-width: 400px;
  margin: 2rem auto;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
label {
  font-weight: bold;
}
input {
  padding: 0.5rem;
  font-size: 1rem;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.