<form action="">
  <!-- ---------- input[type="text"] ---------- -->
  <div class="form_parent">
    <label for="name" class="form_title">名前</label>
    <input
      type="text"
      value=""
      name="name"
      id="name"
      class="form_element"
      placeholder="例:えゔぉ わーくす"
      autocomplete="name"
      required
      aria-invalid="false"
    />
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
  <!-- ---------- input[type="email"] ---------- -->
  <div class="form_parent">
    <label for="mail" class="form_title">メールアドレス</label>
    <input
      type="email"
      value=""
      name="mail"
      id="mail"
      class="form_element"
      placeholder="例:info@evoworx.co.jp"
      autocomplete="email"
      pattern="[A-Za-z0-9._+\-']+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$"
      required
      aria-invalid="false"
    />
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
  <!-- ---------- input[type="tel"] ---------- -->
  <div class="form_parent">
    <label for="tel" class="form_title">電話番号</label>
    <input
      type="tel"
      value=""
      name="tel"
      id="tel"
      class="form_element"
      placeholder="例:03-6417-9340"
      autocomplete="tel"
      pattern="\d{2,4}-?\d{2,4}-?\d{3,4}"
      required
      aria-invalid="false"
    />
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
  <!-- ---------- input[type="date"] ---------- -->
  <div class="form_parent">
    <label for="date" class="form_title">生年月日</label>
    <input type="date" value="" name="date" id="date" class="form_element" min="1900-01-01" max="2000-01-01" required aria-invalid="false" />
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
  <!-- ---------- input[type="url"] ---------- -->
  <div class="form_parent">
    <label for="url" class="form_title">好きなサイトのURL</label>
    <input
      type="url"
      value=""
      name="url"
      id="url"
      class="form_element"
      placeholder="例:https://evoworx.co.jp"
      autocomplete="url"
      pattern="https://evoworx.co.jp"
      required
      aria-invalid="false"
    />
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
  <!-- ---------- input[type="checkbox"] ---------- -->
  <fieldset class="form_parent">
    <legend class="form_title">すきな動物</legend>
    <ul>
      <li class="flex">
        <input type="checkbox" value="いぬ" name="favorite_animal" id="dog" />
        <label for="dog">いぬ</label>
      </li>
      <li class="flex">
        <input type="checkbox" value="ねこ" name="favorite_animal" id="cat" />
        <label for="cat">ねこ</label>
      </li>
      <li class="flex">
        <input type="checkbox" value="うさぎ" name="favorite_animal" id="rabbit" />
        <label for="rabbit">うさぎ</label>
      </li>
    </ul>
  </fieldset>
  <!-- ---------- input[type="radio"] ---------- -->
  <fieldset class="form_parent">
    <legend class="form_title">今日のあさごはん</legend>
    <ul>
      <li class="flex">
        <input type="radio" value="ごはん" name="breakfast" id="rice" class="form_element" required />
        <label for="rice">ごはん</label>
      </li>
      <li class="flex">
        <input type="radio" value="パン" name="breakfast" id="bread" class="form_element" required />
        <label for="bread">パン</label>
      </li>
      <li class="flex">
        <input type="radio" value="めん" name="breakfast" id="noodles" class="form_element" required />
        <label for="noodles">めん</label>
      </li>
      <li class="flex">
        <input type="radio" value="たべていない" name="breakfast" id="not" class="form_element" required />
        <label for="not">たべていない</label>
      </li>
    </ul>
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </fieldset>
  <!-- ---------- select ---------- -->
  <div class="form_parent">
    <label for="select" class="form_title">今日の天気</label>
    <select name="select" id="select" class="form_element" required aria-invalid="false">
      <option value="" selected disabled> 選択してください</option>
      <option value="sunny">晴れ</option>
      <option value="cloudy">くもり</option>
      <option value="rainy">雨</option>
    </select>
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
  <!-- ---------- textarea ---------- -->
  <div class="form_parent">
    <label for="message" class="form_title">今日の一句</label>
    <textarea name="message" id="message" class="form_element" maxlength="20" minlength="10"  required aria-invalid="false"></textarea>
    <span class="error_message" aria-live="off" aria-hidden="true"></span>
  </div>
</form>
/* フォーム要素 */
form {
  display: grid;
  grid-template-columns: minmax(40%, 500px);
  justify-content: center;
  row-gap: 30px;
  margin-inline: 20px;
  padding-block: 60px;
  font-size: 16px;
}
input,
textarea,
select {
  display: block;
}
input:not([type='checkbox']):not([type='radio']) {
  width: 100%;
  min-height: 50px;
}
select {
  min-height: 50px;
}
textarea {
  resize: vertical;
  min-height: 80px;
}
/* フォーム要素の親要素 */
.form_parent {
  display: grid;
  row-gap: 8px;
}
/* フォームの項目名 */
.form_title {
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  justify-content: flex-end;
  column-gap: 8px;
}

/* required属性がある項目名 */
.form_parent:has(:where(input, textarea, select):required) .form_title {
  position: relative;

  &::before {
    content: '必須';
    font-size: 12px;
    padding: 4px;
    color: #fff;
    background-color: #e00000;
  }
}
/* エラーメッセージ */
.error_message {
  display: block;
  color: #e00000;

  &[aria-hidden='true'] {
    display: none;
  }
}
/* ラジオボタンとチェックボックス */
.flex {
  display: flex;
  align-items: center;
}
const FORM_ELEMENT_CLASS = 'form_element'; // フォーム要素のクラス名
const PARENT_CLASS = 'form_parent'; // フォーム要素の親要素のクラス名
const ERROR_MESSAGE_CLASS = 'error_message'; // エラーメッセージ要素のクラス名
const ERROR_CLASS = 'is_error'; // エラー表示用のクラス名
const REQUIRED_CLASS = 'is_required'; // 必須項目表示用のクラス名
const validationEventTypes = ['blur', 'change']; // バリデーションを行うイベント

// エラー状態を追加する
const addErrorState = (parentElement, errorMessageElement, errorMessage, formElement) => {
  parentElement.classList.add(ERROR_CLASS);
  errorMessageElement.textContent = errorMessage;
  errorMessageElement.setAttribute('aria-live', 'polite');
  errorMessageElement.setAttribute('aria-hidden', 'false');
  if (formElement.hasAttribute('aria-invalid')) {
    formElement.setAttribute('aria-invalid', 'true');
  }
};

// エラー状態を削除する
const removeErrorState = (parentElement, errorMessageElement, formElement) => {
  parentElement.classList.remove(ERROR_CLASS);
  errorMessageElement.textContent = '';
  errorMessageElement.removeAttribute('aria-live');
  errorMessageElement.setAttribute('aria-hidden', 'true');
  if (formElement.hasAttribute('aria-invalid')) {
    formElement.setAttribute('aria-invalid', 'false');
  }
};

// バリデーションのエラー状態を更新する
const updateErrorState = (element, showError, errorMessage = element.validationMessage) => {
  const parentElement = element.closest(`.${PARENT_CLASS}`); // フォーム要素の親要素
  const errorMessageElement = parentElement.querySelector(`.${ERROR_MESSAGE_CLASS}`); // エラーメッセージを表示する要素
  if (!parentElement || !errorMessageElement) return;

  if (showError) {
    // エラーの場合
    addErrorState(parentElement, errorMessageElement, errorMessage, element);
  } else {
    // エラーでない場合
    removeErrorState(parentElement, errorMessageElement, element);
  }
};

// バリデーションのハンドリング
const handleErrorValidation = (element) => {
  const validity = element.validity;
  const showError = validity.patternMismatch || validity.rangeOverflow || validity.rangeUnderflow || validity.tooLong || validity.tooShort || validity.typeMismatch || validity.valueMissing;

  updateErrorState(element, showError);
};

// イベントにエラーハンドリングを登録する
const attachValidationHandlers = (element) => {
  validationEventTypes.forEach((eventType) => {
    element.addEventListener(eventType, (event) => {
      const target = event.target;

      handleErrorValidation(target);
    });
  });
};

(() => {
  const formElements = document.querySelectorAll(`.${FORM_ELEMENT_CLASS}`);

  formElements?.forEach((element) => {
    attachValidationHandlers(element);
  });
})();
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.