Web developers could style form elemets with CSS pseudo class & pseudo elements and other modern CSS properties.
		
    
    HTML structure
    
<div class="input-set">
  <input name="input-name" type="text" placeholder="placeholder" />
  <label class="input-set__label">
    <span class="input-set__label__span">label</span>
  </label>
  <em class="input-set__em"></em>
</div>
    
    Style Customization
    Apply accent-color to <input />'s style.
    
<style>
.input-set {
  --icon-warn: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIGhlaWdodD0nMjQnIHdpZHRoPScyNCc+PHBhdGggZD0nTTAgMGgyNHYyNEgweicgZmlsbD0nbm9uZScvPjxwYXRoIGZpbGw9JyNlYjBmMjknIGQ9J00xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0xIDE1aC0ydi0yaDJ2MnptMC00aC0yVjdoMnY2eicvPjwvc3ZnPg==) no-repeat 50% 50%/cover;
  --icon-arrow-down: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScyNCcgaGVpZ2h0PScyNCc+PGcgZmlsbD0nbm9uZScgZmlsbC1ydWxlPSdldmVub2RkJz48cGF0aCBmaWxsPScjNkU3NzgwJyBkPSdNMTguOTI1IDkuNjE0TDEyLjQ5IDE2IDYuMDU1IDkuNjE1YS45MzcuOTM3IDAgMDEwLTEuMzM4Ljk1OC45NTggMCAwMTEuMzQ3IDBsNS4wODggNS4wNDkgNS4wODgtNS4wNDlhLjk1Ni45NTYgMCAwMTEuMzQ3IDAgLjk0Ljk0IDAgMDEwIDEuMzM3eicvPjxwYXRoIGQ9J00yNCAwdjI0SC45OFYweicvPjwvZz48L3N2Zz4=) no-repeat 50% 50%/24px 24px;
  --background-color: var(--default-background-color, rgba(var(--white)));
  --text-color: var(--default-text-color, rgba(var(--batcave)));
  --border-color: var(--default-border-color, rgba(var(--dolphin)));
  --err-color: var(--default-err-color, rgba(var(--solo-cup)));
  --err-text-color: var(--default-err-text-color, rgba(var(--dolphin)));
  --label-text-color: var(--default-label-text-color, rgba(var(--dolphin)));
  --theme: var(--default-theme, rgba(var(--dory)));
  --hover: var(--theme);
  /* readyonly, disabled */
  --background-color-disabled: var(--default-background-color-disabled, rgba(var(--grey-hair)));
  --text-color-disabled: var(--default-text-color-disabled, rgba(var(--bob)));
  --border-color-disabled: var(--default-border-color-disabled, rgba(var(--bob)));
  --placeholder-text-color-normal: transparent;
  --placeholder-text-color-active: var(--default-placeholder-text-color, rgba(var(--charcoal)));
  --placeholder-text-color: var(--placeholder-text-color-normal);
  --border-radius: var(--default-border-radius, 4px);
  --input-block-size: var(--default-input-block-size, 48px);
  --input-line-height: var(--default-input-line-height, 46px);
  --padding-inline-start: var(--default-padding-inline-start, 12px);
  --padding-inline-end: var(--default-padding-inline-end, 12px);
  --padding-inline: var(--padding-inline-start) var(--padding-inline-end);
  --border-size-normal: 1px;
  --border-size-active: 2px;
  --border-size: var(--border-size-normal);
  --err-display: none;
}
/* blank */
.input-set--blank {
  border-radius: var(--border-radius);
  box-sizing: border-box;
  background-color: var(--background-color);
  box-shadow: inset calc(var(--border-size) * -1) calc(var(--border-size) * -1) 0 var(--border-color), inset var(--border-size) var(--border-size) 0 var(--border-color);
  outline: 0 none;
}
.input-set--blank:focus-within {
  --border-color: var(--hover);
  --border-size: var(--border-size-active);
}
.input-set > :is(input, select, textarea)::-webkit-input-placeholder{color:var(--placeholder-text-color);transition:color 100ms ease;will-change:color;}
.input-set > :is(input, select, textarea)::-moz-placeholder{color:var(--placeholder-text-color);transition:color 100ms ease;will-change:color;}
.input-set{position:relative;display:block;}
.input-set > :is(input, select, textarea) {
  font-size:1em;
  inline-size:100%;
  padding-inline:var(--padding-inline);
  block-size:var(--input-block-size);
  border-radius:var(--border-radius);
  box-sizing:border-box;
  display:block;
  appearance:none;
  border:0 none;
  resize:none;
  outline: 0 none;
  line-height: var(--input-line-height);
  color: var(--text-color);
  background-color: var(--background-color);
  box-shadow: inset calc(var(--border-size) * -1) calc(var(--border-size) * -1) 0 var(--border-color), inset var(--border-size) var(--border-size) 0 var(--border-color);
  transition: box-shadow 100ms ease;
}
.input-set > :is(input, select, textarea):focus{outline: 0 none;}
.input-set__label{position:absolute;inset-inline-start:6px;inset-block-start:16px;inline-size:fit-content;font-size:1em;color:var(--label-text-color);line-height:1;padding-inline:6px;display:block;pointer-events:none;transition:transform 100ms ease,color 100ms ease,background-color 100ms ease;will-change:transform,color,background-color;}
.input-set__label__span{position:relative;z-index:1;}
.input-set__label::after{position:absolute;inset-inline-start:0;inset-block-start:50%;inline-size:100%;block-size:3.53px;background-color:var(--background-color);content:'';}
.input-set__em{font-size:14px;line-height:1;color:var(--err-text-color);padding-inline-start:var(--padding-inline-start);margin-block-start:8px;display:var(--err-display);align-items:center;gap:6px;}
.input-set__em::before{inline-size:14px;block-size:14px;background:var(--icon-warn);content:'';}
.input-set--hide-label {
  --placeholder-text-color-normal: var(--placeholder-text-color-active);
}
.input-set--hide-label .input-set__label{display:none;}
/* focus */
.input-set > :is(input, select, textarea):focus {
  --border-color: var(--hover);
  --border-size: var(--border-size-active);
}
.input-set > :is(input, select, textarea):focus::-webkit-input-placeholder {
  --placeholder-text-color: var(--placeholder-text-color-active);
}
.input-set > :is(input, select, textarea):focus::-moz-placeholder {
  --placeholder-text-color: var(--placeholder-text-color-active);
}
.input-set > :is(input, select, textarea):focus ~ .input-set__label {
  color: var(--hover);
  transform: translateY(-24px) scale(0.85);
}
.input-set > :is(input, select, textarea):not(:placeholder-shown) ~ .input-set__label {
  transform: translateY(-24px) scale(0.85);
}
/* invalid */
.input-set > :is(input, select, textarea):invalid~.input-set__em,
.input-set > :is(input, select, textarea)[invalid]~.input-set__em {
  --err-display: flex;
}
.input-set > :is(input, select, textarea):invalid,
.input-set > :is(input, select, textarea)[invalid] {
  --border-color: var(--err-color);
}
/* inert, readonly, disabled */
[inert] .input-set,
.input-set:has(:is(input, select, textarea)[readonly]),
.input-set:has(:is(input, select, textarea)[disabled]) {
  --default-text-color: var(--text-color-disabled);
  --default-border-color: var(--border-color-disabled);
  --default-background-color: var(--background-color-disabled);
}
/* select */
.input-set--select select {
  padding-inline-end: calc(var(--padding-inline-end) * 2 + 24px);
}
.input-set--select::after{position:absolute;inset-inline-end:var(--padding-inline-end);inset-block-start:50%;inline-size:24px;block-size:24px;content:'';margin-block-start:-12px;background:var(--icon-arrow-down);pointer-events:none;}
@media (hover: hover) {
  .input-set:hover {
    --border-color: var(--hover);
  }
  .input-set:hover > :is(input, select, textarea):not(:placeholder-shown)~.input-set__label {
    --label-text-color: var(--hover);
  }
}
@media (prefers-color-scheme: dark) {
  .input-set {
    --default-background-color: rgba(var(--batcave));
    --default-text-color: rgba(var(--grey-hair));
    --default-border-color: rgba(var(--charcoal));
    --default-placeholder-text-color: rgba(var(--dolphin));
    --default-label-text-color: rgba(var(--gandalf));
    --default-placeholder-color: rgba(var(--dolphin));
    --default-text-color-disabled: rgba(var(--shark));
    --default-border-color-disabled: rgba(var(--charcoal));
    --default-background-color-disabled: rgba(var(--batcave));
  }
}
</style>
    
    Events
    Add input and invalid events to main form to display error effect or not.
    
<script type="module">
const controller = new AbortController()
const { signal } = controller
const form = document.querySelector('form');
const transformErrorMessage = (inputEle) => {
  let message = '';
  const validity = inputEle.validity;
  const {
    validityDefault,
    validityRangeOverflow,
    validityRangeUnderflow,
    validityValueMissing
  } = inputEle.dataset;
  switch (true) {
    case validity.valueMissing:
      message = validityValueMissing || '此為必填欄位';
      break
    case validity.rangeOverflow:
      message = validityRangeOverflow || '內容已超過最大值';
      break
    case validity.rangeUnderflow:
      message = validityRangeUnderflow || '內容低於最小值';
      break
    default:
      // patternMismatch, badInput, customError, stepMismatch, tooLong, tooShort, typeMismatch
      message = validityDefault || '格式錯誤,請再確認';
      break
  }
  return message;
}
const onInput = (evt) => {
  const input = evt.target.closest('input');
  if (!input) {
    return;
  }
  input.toggleAttribute('invalid', false);
};
const onInvalid = (evt) => {
  evt.preventDefault();
  const inputSet = evt.target.closest('.input-set');
  const input = evt.target.closest('input');
  
  if (!inputSet) {
    return;
  }
  const errElement = inputSet.querySelector('.input-set__em');
  errElement.textContent = transformErrorMessage(evt.target);
  input.toggleAttribute('invalid', true);
}
// events
form.addEventListener('invalid', onInvalid, { signal, capture: true });
form.addEventListener('input', onInput, { signal });
</script>