Inline Validation
As you type, a tick or a warning slides into the end of the field and a message opens underneath. A right answer shows straight away. A wrong one waits until you pause or leave the field, so it doesn’t nag mid-word. Try both fields.
How to use it
- Paste the snippet just before
</head>. - Wrap an input in an element with
data-inline-validation. - Use the input’s own rules:
type="email",minlength,maxlength,patternorrequired.
Options
Add any of these attributes to the same element.
data-error | The message when it’s wrong. Default: one that fits the rule it breaks. |
data-success | The message when it’s right. Default Looks good. |
data-delay | How long you pause before a warning shows. Default 700 ms. |
data-min | Minimum length, if the input has no minlength. |
data-autoplay | It types half an address, waits for the warning, finishes it and gets the tick, over and over while in view. It stops as soon as you use the field. |
data-text | What autoplay types. Default [email protected]. |
The message is linked to the input with aria-describedby and read out politely, and the input gets aria-invalid. Listen for the validate event: e.detail.valid is true, false or null when empty.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Inline Validation: as you type, a tick or a warning slides into the end
// of the field and a message opens underneath. A right answer shows at
// once; a wrong one waits until you pause (or leave the field), so it
// doesn't nag mid-word.
// Usage: <div data-inline-validation><input type="email" placeholder="Email"></div>
// It checks what the input asks for: type="email", minlength, maxlength,
// pattern and required (required only complains when you leave it empty).
// Optional: data-error (the message when it's wrong; default depends on
// the check), data-success (the message when it's right, default "Looks
// good"), data-delay (pause in ms before a warning shows, default 700),
// data-min (minimum length, if the input has no minlength), data-autoplay
// (it types a half-finished value, waits for the warning, finishes it and
// gets the tick, over and over while in view; it stops as soon as you use
// the field), data-text (what autoplay types, default "[email protected]").
// Listen for the "validate" event: e.detail.valid is true, false or null (empty).
var DEFAULTS = { success: 'Looks good', delay: 700, text: '[email protected]' };
var OK = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#16a34a"/>' +
'<path class="ilv-tick" d="M7 12.5l3.3 3.3L17 9" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var BAD = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#dc2626"/>' +
'<path d="M12 6.8v6.4" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/><circle cx="12" cy="17.2" r="1.5" fill="#fff"/></svg>';
var css =
'.ilv{text-align:left}' +
'.ilv-box{position:relative}' +
'.ilv .ilv-field{padding-right:2.8em}' +
'.ilv.ilv-ok .ilv-field{border-color:#16a34a;box-shadow:0 0 0 3px rgba(22,163,74,.12)}' +
'.ilv.ilv-err .ilv-field{border-color:#dc2626;box-shadow:0 0 0 3px rgba(220,38,38,.12)}' +
'.ilv-icon{position:absolute;top:50%;right:.85em;width:1.3em;height:1.3em;margin-top:-.65em;pointer-events:none}' +
'.ilv-icon svg{position:absolute;inset:0;width:100%;height:100%;opacity:0;overflow:visible}' +
'.ilv-msg{height:0;overflow:hidden}' +
'.ilv-msg-in{display:block;padding-top:.5em;font-size:.86em;line-height:1.35}' +
'.ilv-ok .ilv-msg-in{color:#15803d}.ilv-err .ilv-msg-in{color:#dc2626}' +
'.ilv-text{white-space:pre}' +
'.ilv-caret{display:inline-block;width:1.5px;height:1.1em;margin-left:1px;vertical-align:-0.2em;background:currentColor;animation:ilv-blink 1s steps(1) infinite}' +
'@keyframes ilv-blink{50%{opacity:0}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function setup(el) {
var d = el.dataset;
var input = el.querySelector('input, textarea');
var field = input || el.querySelector('[data-field]');
if (!field) return;
var delay = d.delay !== undefined ? parseInt(d.delay, 10) : DEFAULTS.delay;
el.classList.add('ilv');
field.classList.add('ilv-field');
var box = document.createElement('div');
box.className = 'ilv-box';
field.parentNode.insertBefore(box, field);
box.appendChild(field);
var icon = document.createElement('span');
icon.className = 'ilv-icon';
icon.setAttribute('aria-hidden', 'true');
icon.innerHTML = OK + BAD;
box.appendChild(icon);
var okIcon = icon.firstChild, badIcon = icon.lastChild;
var tick = anime.svg.createDrawable(okIcon.querySelector('.ilv-tick'));
var msg = document.createElement('div');
msg.className = 'ilv-msg';
msg.innerHTML = '<span class="ilv-msg-in"></span>';
var msgIn = msg.firstChild;
el.appendChild(msg);
if (input) {
msgIn.id = 'ilv-' + Math.random().toString(36).slice(2, 9);
msgIn.setAttribute('aria-live', 'polite');
input.setAttribute('aria-describedby', ((input.getAttribute('aria-describedby') || '') + ' ' + msgIn.id).trim());
}
// The mock field (thumbs) gets typed into by autoplay.
var text = null, caret = null, value = '';
if (!input) {
field.textContent = '';
text = document.createElement('span');
text.className = 'ilv-text';
field.appendChild(text);
}
var kind = input ? (input.getAttribute('type') || 'text') : 'email';
var min = input && input.minLength > 0 ? input.minLength : parseInt(d.min, 10) || 0;
var max = input && input.maxLength > 0 ? input.maxLength : 0;
var pattern = input && input.getAttribute('pattern');
var get = function () { return input ? input.value : value; };
var set = function (v) {
if (input) input.value = v;
else {
value = v;
text.textContent = v;
if (!caret) { caret = document.createElement('span'); caret.className = 'ilv-caret'; field.appendChild(caret); }
}
check(false);
};
// What's wrong with v, or '' when it's fine.
function problem(v) {
if (min && v.length < min) return 'Use at least ' + min + ' characters.';
if (max && v.length > max) return 'Use ' + max + ' characters or fewer.';
if (kind === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return 'Enter an email like [email protected].';
if (kind === 'url' && !/^https?:\/\/[^\s.]+\.[^\s]{2,}/.test(v)) return 'Enter a web address starting with https://';
if (pattern) {
try { if (!new RegExp('^(?:' + pattern + ')$').test(v)) return (input && input.title) || 'Use the format asked for.'; } catch (e) {}
}
return '';
}
var state = '', timer = null;
function show(next, message) {
if (next === state && (msgIn.textContent === message || !next)) return;
var prev = state;
state = next;
el.classList.toggle('ilv-ok', next === 'ok');
el.classList.toggle('ilv-err', next === 'err');
if (input) input.setAttribute('aria-invalid', next === 'err' ? 'true' : 'false');
el.dispatchEvent(new CustomEvent('validate', { bubbles: true, detail: { valid: next ? next === 'ok' : null } }));
var gone = prev === 'ok' ? okIcon : prev === 'err' ? badIcon : null;
var come = next === 'ok' ? okIcon : next === 'err' ? badIcon : null;
var startH = msg.offsetHeight;
if (next) msgIn.textContent = message;
var endH = next ? msgIn.offsetHeight : 0;
if (reduced()) {
anime.utils.set([okIcon, badIcon], { opacity: 0 });
if (come) anime.utils.set(come, { opacity: 1, x: 0, scale: 1 });
if (next === 'ok') anime.utils.set(tick, { draw: '0 1' });
msg.style.height = next ? 'auto' : '0px';
return;
}
if (gone && gone !== come) {
anime.utils.remove(gone);
anime.animate(gone, { opacity: 0, scale: 0.4, y: '0.5em', duration: 200, ease: 'in(2)' });
}
if (come && come !== gone) {
anime.utils.remove(come);
anime.animate(come, { opacity: [0, 1], x: ['1.2em', 0], y: [0, 0], scale: [0.4, 1], rotate: next === 'err' ? [-25, 0] : [0, 0], duration: 460, ease: 'outBack(2.2)' });
if (next === 'ok') anime.animate(tick, { draw: ['0 0', '0 1'], duration: 360, delay: 180, ease: 'out(2)' });
else anime.animate(come, { rotate: [0, -14, 12, -6, 0], duration: 500, delay: 420, ease: 'out(2)' });
}
anime.utils.remove([msg, msgIn]);
anime.animate(msg, {
height: [startH, endH], duration: next ? 320 : 240, ease: next ? 'out(3)' : 'inOut(2)',
onComplete: function () { msg.style.height = state ? 'auto' : '0px'; }
});
if (next && next !== prev) anime.animate(msgIn, { opacity: [0, 1], y: ['-0.4em', 0], duration: 320, delay: 60, ease: 'out(3)' });
}
function check(leaving) {
clearTimeout(timer);
var v = get();
if (!v) {
if (leaving && input && input.required) show('err', d.error || 'This field is required.');
else show('', '');
return;
}
var p = problem(v);
if (!p) { show('ok', d.success || DEFAULTS.success); return; }
if (state === 'ok') show('', '');
var say = d.error || p;
if (leaving || state === 'err') show('err', say);
else timer = setTimeout(function () { show('err', say); }, delay);
}
if (input) {
input.addEventListener('input', function () { stop(); check(false); });
input.addEventListener('blur', function () { check(true); });
input.addEventListener('focus', stop);
el.addEventListener('pointerdown', stop);
if (input.value) check(false);
}
var auto = null, stopped = false;
function stop() {
if (stopped) return;
stopped = true;
clearTimeout(auto);
if (input && document.activeElement !== input) { input.value = ''; check(false); }
}
if (on(d.autoplay) && !reduced()) {
var word = d.text || DEFAULTS.text;
var at = word.indexOf('@');
var cut = at > 0 ? Math.min(word.length - 1, at + 4) : Math.ceil(word.length * 0.5);
var steps = [];
for (var i = 1; i <= word.length; i++) {
(function (n) { steps.push([function () { set(word.slice(0, n)); }, n === cut ? delay + 1300 : 70 + Math.random() * 90]); })(i);
}
steps.push([function () {}, 1800]);
for (var j = word.length - 1; j >= 0; j--) {
(function (n) { steps.push([function () { set(word.slice(0, n)); }, 28]); })(j);
}
steps.push([function () {}, 900]);
var run = function (k) {
if (stopped) return;
steps[k][0]();
auto = setTimeout(function () { run((k + 1) % steps.length); }, steps[k][1]);
};
new IntersectionObserver(function (entries) {
clearTimeout(auto);
if (stopped) return;
if (entries[0].isIntersecting) { set(''); auto = setTimeout(function () { run(0); }, 600); }
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-inline-validation]').forEach(function (el) {
if (el._ilv) return;
el._ilv = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-inline-validation>
<input type="email" placeholder="Email address">
</div>
<div data-inline-validation data-error="Use 4 or more characters.">
<input type="text" placeholder="Username" minlength="4">
</div>