← Forms

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

  1. Paste the snippet just before </head>.
  2. Wrap an input (or textarea) in an element with data-input-underline-grow.
  3. Style the field however you like. A borderless field looks best.

Options

Add any of these attributes to the same element.

data-colorThe line colour. Default #6a0dad.
data-thicknessThe line thickness in px. Default 2.
data-fromWhere the line grows from: center, left or right. Default center.
data-durationHow long the line takes to grow. Default 450 ms.
data-autoplayIt focuses, types and moves on over and over while in view, and stops as soon as you use the field.
data-textWhat autoplay types. Default Ada Lovelace.
1 · Snippet
<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>
2 · Your HTML
<div data-input-underline-grow>
  <input type="text" placeholder="Your name">
</div>