Input Underline Grow
Focus the field and a coloured underline grows out from its centre to both edges. Leave it and the line draws back into the middle. A faint base line stays put underneath.
How to use it
- Paste the snippet just before
</head>. - Wrap an input (or textarea) in an element with
data-input-underline-grow. - Style the field however you like. A borderless field looks best.
Options
Add any of these attributes to the same element.
data-color | The line colour. Default #6a0dad. |
data-thickness | The line thickness in px. Default 2. |
data-from | Where the line grows from: center, left or right. Default center. |
data-duration | How long the line takes to grow. Default 450 ms. |
data-autoplay | It focuses, types and moves on over and over while in view, and stops as soon as you use the field. |
data-text | What autoplay types. Default Ada Lovelace. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Input Underline Grow: focus the field and a coloured underline grows out
// from its centre to both edges; leave it and the line draws back into the
// middle and fades. A faint base line stays put underneath.
// Usage: <div data-input-underline-grow><input placeholder="Your name"></div>
// The wrapper holds one input or textarea. Style the field however you
// like (a borderless field looks best).
// Optional: data-color (the line colour, default #6a0dad), data-thickness
// (px, default 2), data-duration (ms, default 450), data-from ("center",
// "left" or "right"; where the line grows from, default center),
// data-autoplay (it focuses, types data-text and leaves, over and over
// while in view; it stops as soon as you use the field), data-text (what
// autoplay types, default "Ada Lovelace").
var DEFAULTS = { color: '#6a0dad', thickness: 2, duration: 450, from: 'center', text: 'Ada Lovelace' };
var css =
'.iug{position:relative}' +
'.iug-base,.iug-line{position:absolute;left:0;right:0;bottom:0;pointer-events:none}' +
'.iug-base{height:1px;background:currentColor;opacity:0.22}' +
'.iug-line{border-radius:99px;transform:scaleX(0)}' +
'.iug-text{white-space:pre}' +
'.iug-caret{display:none;width:1.5px;height:1.15em;margin-left:1px;vertical-align:middle;background:currentColor;animation:iug-blink 1s steps(1) infinite}' +
'.iug-focus .iug-caret{display:inline-block}' +
'@keyframes iug-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]') || el.firstElementChild;
if (!field) return;
var real = !!input;
var dur = parseFloat(d.duration) || DEFAULTS.duration;
var from = d.from || DEFAULTS.from;
el.classList.add('iug');
var base = document.createElement('span');
base.className = 'iug-base';
var line = document.createElement('span');
line.className = 'iug-line';
line.style.height = (parseFloat(d.thickness) || DEFAULTS.thickness) + 'px';
line.style.background = d.color || DEFAULTS.color;
line.style.transformOrigin = from === 'left' ? '0 50%' : from === 'right' ? '100% 50%' : '50% 50%';
base.setAttribute('aria-hidden', 'true');
line.setAttribute('aria-hidden', 'true');
el.appendChild(base);
el.appendChild(line);
var text = null;
if (!real) {
text = document.createElement('span');
text.className = 'iug-text';
text.textContent = field.textContent;
field.textContent = '';
field.appendChild(text);
var caret = document.createElement('span');
caret.className = 'iug-caret';
field.appendChild(caret);
}
var set = function (v) { if (real) input.value = v; else text.textContent = v; };
// Keep the lines on the field's bottom edge, wherever it sits in the wrapper.
function measure() {
var bottom = el.clientHeight - (field.offsetTop + field.offsetHeight);
base.style.bottom = line.style.bottom = Math.max(0, bottom) + 'px';
base.style.left = line.style.left = field.offsetLeft + 'px';
base.style.right = line.style.right = Math.max(0, el.clientWidth - field.offsetLeft - field.offsetWidth) + 'px';
}
measure();
if (window.ResizeObserver) new ResizeObserver(measure).observe(el);
function focus(v) {
el.classList.toggle('iug-focus', v);
if (reduced()) { anime.utils.set(line, { scaleX: v ? 1 : 0, opacity: 1 }); return; }
if (v) anime.animate(line, { scaleX: [0, 1], opacity: [1, 1], duration: dur, ease: 'out(4)' });
else anime.animate(line, { scaleX: 0, opacity: [1, 0.3], duration: dur * 0.75, ease: 'inOut(3)' });
}
if (real) {
input.addEventListener('focus', function () { stop(); focus(true); });
input.addEventListener('blur', function () { focus(false); });
input.addEventListener('pointerdown', stop);
}
// Autoplay: a pretend visitor focuses, types, and moves on.
var timer = null, stopped = false, playing = false;
function stop() {
if (stopped) return;
stopped = true;
clearTimeout(timer);
if (playing && (!real || document.activeElement !== input)) { focus(false); set(''); }
}
// Using any other field on the page ends the pretend visitor too.
if (on(d.autoplay)) document.addEventListener('focusin', function (e) {
if (e.target !== input && e.target.matches && e.target.matches('input, textarea, select')) stop();
});
if (on(d.autoplay) && !reduced()) {
var word = d.text || DEFAULTS.text;
var steps = [[function () { playing = true; focus(true); }, 650]];
for (var i = 1; i <= word.length; i++) {
(function (n) { steps.push([function () { set(word.slice(0, n)); }, 60 + Math.random() * 80]); })(i);
}
steps.push([function () {}, 1100]);
steps.push([function () { focus(false); }, 900]);
steps.push([function () { set(''); playing = false; }, 700]);
var run = function (i) {
if (stopped) return;
steps[i][0]();
timer = setTimeout(function () { run((i + 1) % steps.length); }, steps[i][1]);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (stopped) return;
if (entries[0].isIntersecting) {
set('');
el.classList.remove('iug-focus');
anime.utils.set(line, { scaleX: 0 });
timer = setTimeout(function () { run(0); }, 500);
}
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-input-underline-grow]').forEach(function (el) {
if (el._iug) return;
el._iug = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-input-underline-grow>
<input type="text" placeholder="Your name">
</div>