<div class="container">
	<div class="col-container">
		<h1>Longwinded Custom Validations</h1>
		<p>... with Constraint Validation and <code>ElementInternals</code> API
		<p>For demonstration's sake: a totally contrived example of an input expecting "Hello"</p>
		<label for="validation-select">Validation Type</label>
		<select id="validation-select" name="validation-type">
			<option value="live" selected>Live (immediate)</option>
			<option value="after">On submission</option>
			<option value="blur">Late validation (onblur)</option>
			<option selected value="none">None (bypass validation)</option>
		</select>
		<output class="output">
		</output>

		<fieldset>
			<legend>Legend</legend>
			<li>Default, on-submit validation: "Please fill out this field."</li>
			<li>Live validation: "You must say hello!"</li>
			<li> Late validation (after leaving field): "You didn't enter hello after leaving the field!"</legend>
		</fieldset>

		<form id="example-form" method="post" onsubmit="handleSubmit(event)">
			<div class="fields">
				<div class="field">
					<label for="a">Greeting<span data-matches="required">*</span></label>
					<input id="a" required aria-required name="a" pattern="hel{l,2}o" aria-describedby="input-required" placeholder="Say 'hello' " aria-placeholder=" Enter hello" minlength="5" maxlength="5" />
					<div id="input-required" class="message">
						<ul class="validation" role="list">
							<li data-matches="valid">Currently <code>:valid</code></li>
							<li data-matches="aria-invalid">
								Currently <code>:aria-invalid</code>
							</li>
							<li data-matches="invalid">Currently <code>:invalid</code></li>
							<li data-matches="user-valid">Currently <code>:user-valid</code></li>
							<li data-matches="user-invalid">Currently <code>:user-invalid</code></li>
						</ul>
					</div>
				</div>

				<div class="field">
					<label for="b">Control field <span data-matches="optional">(optional)</span></label>
					<input id="b" name="b" aria-describedby="field-optional" />
					<ul class="validation" role="list">
						<li data-matches="valid">Currently <code>:valid</code></li>
						<li data-matches="aria-invalid">
							Currently <code>:aria-invalid</code>
						</li>
						<li data-matches="invalid">Currently <code>:invalid</code></li>
						<li data-matches="user-valid">Currently <code>:user-valid</code></li>
						<li data-matches="user-invalid">Currently <code>:user-invalid</code></li>
					</ul>
				</div>

			</div>
			<button type="submit">Submit</button>

			<button type="reset">Reset</button>
	</div>
</div>

</div>

</form>
</div>
</div>
/* Original HTML/CSS from https://codepen.io/web-dot-dev/pen/wvNJGrO
See https://web.dev/articles/user-valid-and-user-invalid-pseudo-classes */

input:user-valid {
	--state-color: lightgreen;
}
input:valid {
	--state-color: green;
}
input:invalid {
	--state-color: red;
}
input[aria-invalid="true"],
input::aria-invalid {
	--state-color: orange;
}
[aria-invalid="true"] ~ .errormessage,
[aria-errormessage] {
	visibility: visible;
}
input:user-invalid {
	--state-color: pink;
}

label + input:required {
	content: "*";
}

label::after + input:optional {
	content: " (optional)";
}
.container {
	display: flex;
	flex-direction: row;
}
.col-container {
	display: flex;
	flex-direction: column;
}
output {
	margin-top: 2rem;
	margin-left: 0.25rem;
	height: 100%;
	border: 1px solid gray;
	border-radius: 4px;
	word-wrap: break-word;
}
fieldset {
	margin-top: 1rem;
	margin-bottom: 1rem;
}
form {
	margin: unset;
}
.fields {
	display: flex;
	flex-direction: row;
	gap: 1rem;
}
.field {
	display: flex;
	flex-flow: column nowrap;
	margin-bottom: 0.5em;
}
const form = document.getElementById("example-form");
const requiredTextField = document.getElementById("a");
const output = document.querySelector("output");

const setValidation = (msg) => {
	if (!msg) requiredTextField.setCustomValidity("");
	else {
		requiredTextField.setCustomValidity(msg);
		requiredTextField.validationMessage = msg;
	}
};

function setErrorIfInvalid(id, searchTerm, msg) {
	const element = document.getElementById(id);

	if (!element.value && element.required) {
		setValidation(msg);
	}
	if (element.value.includes(searchTerm)) {
		setValidation();
	}
}
const handleSubmit = (event) => {
	event.preventDefault();
	const { a } = form.elements;
	const pre = document.createElement("pre");
	pre.textContent = JSON.stringify(
		{
			isFormValid: form.checkValidity(),
			a: {
				value: a.value,
				validationMessage: a.validationMessage,
				// willValidate when form is submitted
				willValidate: a.willValidate
			},
			b: b.value
		},
		null,
		2
	);
	output.appendChild(pre);
};

const removeEventListening = () => {
	requiredTextField.removeEventListener("blur", setErrorIfInvalid, true);
	requiredTextField.removeEventListener("input", setErrorIfInvalid, true);
};

const removeAllValidation = () => {
	removeEventListening();
	requiredTextField.classList.remove("invalid");
	requiredTextField.classList.remove("valid");
	requiredTextField.classList.remove("user-invalid");
	requiredTextField.setCustomValidity("");
	requiredTextField.removeAttribute("invalid");
	optionalTextField.removeAttribute("valid");
	requiredTextField.removeAttribute("ariaInvalid");
};

document.getElementById("validation-select").addEventListener("change", (e) => {
	const selectedValue = e.target.value;
	switch (selectedValue) {
		case "after":
			setValidation("");
			removeAllValidation();
			break;
		case "live":
			setValidation("");
			requiredTextField.addEventListener("input", () => {
				setErrorIfInvalid("a", "hello", "You must say hello");
				requiredTextField.reportValidity();
			});
			break;
		case "blur":
			requiredTextField.addEventListener("blur", (e) => {
				setErrorIfInvalid(
					"a",
					"hello",
					"You didn't enter 'hello' after leaving the field!"
				);
				requiredTextField.reportValidity();
			});
			break;
		case "none":
			requiredTextField.setAttribute("formNoValidate", true);
			form.setAttribute("noValidate", true);
			removeAllValidation();
			break;
		default:
			break;
	}
});
Run Pen

External CSS

  1. https://codepen.io/morewry/pen/rNPLxmW/6db00374ef44aae2c249e8f634bd3f07.css

External JavaScript

This Pen doesn't use any external JavaScript resources.