JSFiddle - React, Tailwind, and code Playground

HTML

<label>
    <input type="email" name="test1" />
</label>
<label>
    <input type="password" pattern=".{8}.+*" name="test2" required="required" />
</label>
<label>
    <select id="example" name="test" required="required">
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>    
</label>
<label>
    <textarea name="test" required="required"></textarea>
</label>

CSS

label {
    display: block;
    margin: 1rem;
}

label:after {
	content: attr(data-invalid);
    color: red;
    font-size: x-small;
}

JavaScript

(function() {

	var ATTRIBUTE_NAME = "data-invalid";

	var labels = document.getElementsByTagName("label");
	
	function getLabelOf(element) {
		if (element.id) {
			var index = labels.length;
			while (--index >= 0) {
				var label = labels[index];
				if (label.htmlFor === element.id) {
					return label;
				}
			}
		}
		var ancestor = element.parentNode;
		while (ancestor) {
			if (ancestor.tagName === "LABEL") {
				return ancestor;
			} else {
				ancestor = ancestor.parentNode;
			}
		}
		return null;
	}

	function updateValidityAttributeOf(element) {
		var label = getLabelOf(element);
		if (label) {
			if (element.validity.valid) {
				label.removeAttribute(ATTRIBUTE_NAME);
			} else {
				label.setAttribute(ATTRIBUTE_NAME, element.validationMessage);
			}
		}
	}

	[HTMLSelectElement, HTMLInputElement, HTMLTextAreaElement].forEach(function(element) {
		var prototype = element.prototype;
		var setCustomValidity = prototype.setCustomValidity;
		prototype.setCustomValidity = function(message) {
			setCustomValidity.call(this, message);
			updateValidityAttributeOf(this);
		};
	});

	document.addEventListener("input", function(event) {
		updateValidityAttributeOf(event.target);
	}, false);

	var inputs = document.getElementsByTagName("input");
	var selects = document.getElementsByTagName("select");
	var textareas = document.getElementsByTagName("textarea");
	var forEach = [].forEach;

	document.addEventListener("DOMContentLoaded", forEach.bind([inputs, selects, textareas], function(nodelist) {
		forEach.call(nodelist, updateValidityAttributeOf);
	}), false);

}());


document.addEventListener("DOMContentLoaded", function example() {
    document.getElementById("example").setCustomValidity("CUSTOM!");
}, false);