← AI

Prompt Box Glow

A gradient glow circles the prompt box. Click into it and a thin ring of colour turns slowly round the border with a soft halo; send a prompt and it spins faster and brighter while the answer is worked out, then fades away.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-prompt-box-glow to the box that wraps your input or textarea. Give it a border radius; the glow follows it.
  3. Add data-working while your answer is on its way and remove it when it arrives. Pressing Enter or clicking a button in the box also shows the working glow for a moment.

Options

Add any of these attributes to the same element.

data-colorsComma-separated colours. Default #8b5cf6,#ec4899,#f59e0b,#22d3ee.
data-widthThickness of the ring. Default 2 px.
data-workingSpin fast and glow bright, as if working. Add or remove it any time.
data-workHow long the working glow lasts after Enter or a button click. Default 2800 ms.
data-autoplayType a pretend prompt, send it and clear it, over and over, until someone really clicks into the box.
data-promptsPrompts for the autoplay to type, separated by |.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Prompt Box Glow: a gradient glow circles a prompt box. When the box has
    // focus a thin ring of colour turns slowly round its border with a soft halo;
    // while it's working (after sending) the ring spins faster and the halo
    // brightens, then it all fades away. Follows the box's rounded corners.
    // Usage: <div data-prompt-box-glow><textarea placeholder="Ask anything"></textarea></div>
    // Optional: data-colors (comma-separated, default "#8b5cf6,#ec4899,#f59e0b,#22d3ee"),
    // data-width (ring thickness in px, default 2), data-working (glow as if
    // working; add or remove the attribute any time), data-autoplay (types a
    // pretend prompt, sends it, works for a moment and clears it, over and over;
    // stops once someone really uses the box), data-prompts (prompts to type,
    // separated by |), data-work (ms the pretend work lasts, default 2800).
    // Pressing Enter in the box, or clicking a button inside it, shows the
    // working glow for data-work ms.
    var DEFAULTS = {
      colors: '#8b5cf6,#ec4899,#f59e0b,#22d3ee', width: 2, work: 2800,
      prompts: 'Plan a weekend in Lisbon|Explain black holes simply|Write a haiku about coffee'
    };

    var css =
      '@property --pbg-a{syntax:"<angle>";inherits:true;initial-value:0deg}' +
      '[data-prompt-box-glow]{position:relative;transition:border-color .4s}' +
      '.pbg-fx{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;' +
      'transition:opacity .5s ease;animation:pbg-spin 4s linear infinite}' +
      '.pbg-focus>.pbg-fx{opacity:1}' +
      '.pbg-work>.pbg-fx{opacity:1;animation-duration:1.3s}' +
      '.pbg-ring,.pbg-halo{position:absolute;border-radius:inherit;' +
      'background:conic-gradient(from var(--pbg-a),var(--pbg-stops));' +
      '-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;' +
      'mask:linear-gradient(#000 0 0) content-box exclude,linear-gradient(#000 0 0)}' +
      '.pbg-ring{inset:calc(var(--pbg-w) * -1);padding:var(--pbg-w);border-radius:inherit}' +
      // The halo is blurred, then masked so it only glows outside the box.
      '.pbg-blur{position:absolute;inset:-32px;padding:32px;filter:blur(12px);opacity:.5;transition:opacity .5s,filter .5s;' +
      '-webkit-mask:var(--pbg-hole) center/100% 100% no-repeat content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;' +
      'mask:var(--pbg-hole) center/100% 100% no-repeat content-box exclude,linear-gradient(#000 0 0)}' +
      '.pbg-work .pbg-blur{opacity:.85;filter:blur(16px)}' +
      '.pbg-halo{inset:24px;padding:8px;border-radius:24px}' +
      '@keyframes pbg-spin{to{--pbg-a:360deg}}' +
      '@media (prefers-reduced-motion: reduce){.pbg-fx{animation:none}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var cols = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
      var fx = document.createElement('span');
      fx.className = 'pbg-fx';
      fx.setAttribute('aria-hidden', 'true');
      fx.style.setProperty('--pbg-stops', cols.concat(cols[0]).join(','));
      fx.style.setProperty('--pbg-w', (+d.width || DEFAULTS.width) + 'px');
      // A rounded hole the shape of the box, so the halo never shows inside it.
      var r = parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0;
      fx.style.setProperty('--pbg-hole', 'url("data:image/svg+xml,' + encodeURIComponent(
        '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" rx="' + r + '"/></svg>') + '")');
      fx.innerHTML = '<span class="pbg-blur"><span class="pbg-halo"></span></span><span class="pbg-ring"></span>';
      el.insertBefore(fx, el.firstChild);
      var input = el.querySelector('textarea, input, [contenteditable], [data-pbg-text]');
      var st = { input: input, timers: [], real: false, workTimer: null };
      el._pbg = st;
      var work = +d.work || DEFAULTS.work;

      function sync() {
        el.classList.toggle('pbg-focus', el.contains(document.activeElement) || st.fakeFocus);
        el.classList.toggle('pbg-work', d.working !== undefined || st.fakeWork);
      }
      st.sync = sync;
      st.send = function () {
        st.fakeWork = true;
        sync();
        clearTimeout(st.workTimer);
        st.workTimer = setTimeout(function () {
          st.fakeWork = false;
          if (st.input && 'value' in st.input && st.real) st.input.value = '';
          sync();
        }, work);
      };

      function takeOver() {
        if (st.real) return;
        st.real = true;
        st.timers.forEach(clearTimeout);
        st.fakeFocus = st.fakeWork = false;
        if (st.input) {
          if ('value' in st.input) st.input.value = '';
          else if (st.input.hasAttribute('data-pbg-text')) st.input.textContent = '';
        }
        sync();
      }

      el.addEventListener('focusin', function () { takeOver(); sync(); });
      el.addEventListener('focusout', function () { setTimeout(sync, 0); });
      el.addEventListener('keydown', function (e) {
        if (e.key === 'Enter' && !e.shiftKey && st.input && 'value' in st.input) {
          e.preventDefault();
          if (st.input.value.trim()) st.send();
        }
      });
      el.addEventListener('click', function (e) {
        var b = e.target.closest('button');
        if (b && el.contains(b)) { takeOver(); st.send(); }
      });
      new MutationObserver(sync).observe(el, { attributes: true, attributeFilter: ['data-working'] });
      sync();
    }

    // The pretend user: focus, type a prompt, send it, wait, clear, repeat.
    function autoplay(el) {
      var st = el._pbg, d = el.dataset;
      var prompts = (d.prompts || DEFAULTS.prompts).split('|');
      var work = +d.work || DEFAULTS.work;
      var k = 0;
      function write(text) {
        if (!st.input) return;
        if ('value' in st.input) st.input.value = text;
        else st.input.textContent = text;
      }
      function later(fn, ms) { st.timers.push(setTimeout(function () { if (!st.real) fn(); }, ms)); }
      function round() {
        st.timers = [];
        var text = prompts[k++ % prompts.length], i = 0;
        st.fakeFocus = true;
        st.sync();
        function type() {
          write(text.slice(0, ++i));
          if (i < text.length) later(type, 45 + Math.random() * 70);
          else later(function () {
            st.fakeFocus = false;
            st.send();
            later(function () { write(''); }, 250);
            later(round, work + 1600);
          }, 600);
        }
        later(type, 700);
      }
      round();
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          autoplay(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-prompt-box-glow]').forEach(function (el) {
        if (el._pbg) return;
        setup(el);
        if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-prompt-box-glow style="border-radius: 20px">
  <textarea placeholder="Ask anything"></textarea>
  <button type="button">Send</button>
</div>