<form action="" x-data="form" @submit="submit">
  <h1>Log In</h1>
  <label for="username">Username</label>
  <input name="username" id="username" type="text" x-bind:class="{'invalid':username.errorMessage && username.blurred}" data-rules='["required"]' @blur="blur" @input="input">
  <p x-show="username.errorMessage && username.blurred" x-text="username.errorMessage"></p>

  <label for="email">Email</label>
  <input name="email" type="email" id="email" x-bind:class="{'invalid':email.errorMessage && email.blurred}" data-rules='["required","email"]' @blur="blur" @input="input">
  <p x-show="email.errorMessage && email.blurred" x-text="email.errorMessage"></p>

  <label for="password">Password</label>
  <input name="password" type="password" id="password" x-bind:class="{'invalid':password.errorMessage && password.blurred}" data-rules='["required","minimum:8"]' @blur="blur" @input="input">
  <p x-show="password.errorMessage && password.blurred" x-text="password.errorMessage"></p>

  <label for="passwordConf">Confirm Password</label>
  <input name="passwordConf" type="password" id="passwordConf" x-bind:class="{'invalid':passwordConf.errorMessage && passwordConf.blurred}" data-rules='["required","minimum:8"]' @blur="blur" @input="input">
  <p x-show="passwordConf.errorMessage && passwordConf.blurred" x-text="passwordConf.errorMessage"></p>

  <input type="submit">
</form>
* {
  font-family: "Inter", sans-serif;
}
form {
  width: 30%;
  min-width: 350px;
  margin: auto;
}
label {
  font-size: 1.5em;
}

input {
  width: 100%;
  display: block;
  font-size: 2em;
  border: solid 4px;
  border-color: hsl(210, 100%, 30%);
  margin-bottom: 1.5em;
}
input[type="submit"] {
  width: fit-content;
  font-size: 1.5em;
}
input[type="submit"]:focus {
  background-color: hsl(210, 100%, 80%);
}
.invalid {
  border-color: darkred;
  background-color: hsl(0, 30%, 95%);
  margin-bottom: 0em;
}
.error-message {
  margin-bottom: 1em;
  color: hsl(0deg, 100%, 15%);
}
import Alpine from "https://cdn.skypack.dev/alpinejs";
import kingshottIodine from "https://cdn.skypack.dev/@kingshott/iodine";

Alpine.data("form", form);
Alpine.start();

function form() {
  return {
    username: { errorMessage: "", blurred: false },
    email: { errorMessage: "", blurred: false },
    password: { errorMessage: "", blurred: false },
    passwordConf: { errorMessage: "", blurred: false },
    blur: function (event) {
      let ele = event.target;
      this[ele.name].blurred = true;
      let rules = JSON.parse(ele.dataset.rules);
      this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);
    },
    input: function (event) {
      let ele = event.target;
      let rules = JSON.parse(ele.dataset.rules);
      this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);
    },
    submit: function (event) {
      let inputs = [...this.$el.querySelectorAll("input[data-rules]")];
      inputs.map((input) => {
        if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {
          event.preventDefault();
        }
      });
    },
    getErrorMessage: function (value, rules) {
      let isValid = Iodine.is(value, rules);
      if (isValid !== true) {
        return Iodine.getErrorMessage(isValid);
      }
      return "";
    }
  };
}

External CSS

  1. https://fonts.googleapis.com/css2?family=Inter:wght@500&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.