← AI

Prompt Expand

A short prompt grows into a detailed one. New words open up between the old ones, pushing the text apart as they slide up into place with a soft highlight that fades once they’ve settled, the way an “enhance prompt” button rewrites a quick idea into something richer.

A fluffy ginger cat curled up asleep on a sunny windowsill, soft morning light through lace curtains, a steaming mug beside it, shallow depth of field, warm film photo

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-prompt-expand to an element holding the full, detailed prompt.
  3. Wrap every added part in <ins>. It starts as just the words outside them.

Options

Add any of these attributes to the same element.

data-delayHow long the short prompt shows first. Default 1400 ms.
data-staggerGap between new words. Default 70 ms.
data-colorColour of the highlight on new words. Default #7c3aed.
data-loopShrink back and expand again.
data-holdHow long the full prompt shows before looping. Default 2800 ms.

While it expands the element has the class prx-busy, and prx-done after, so you can style a button next to it. Call animacious.promptExpand(el, true) to expand it from your own button, and false to shrink it back.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Prompt Expand: a short prompt grows into a detailed one. New words slide
    // in between the old ones, pushing the text apart as they open up, with a
    // soft highlight that fades once they've settled, like an "enhance prompt"
    // button at work.
    // Usage: <p data-prompt-expand>A <ins>fluffy ginger</ins> cat <ins>curled up</ins> on a windowsill</p>
    // Write the full, detailed prompt and wrap every added part in <ins>; it
    // starts as just the words outside the <ins> tags.
    // Optional: data-delay (ms the short prompt shows first, default 1400),
    // data-stagger (ms between new words, default 70), data-color (highlight
    // colour, default #7c3aed), data-loop (shrink back and expand again),
    // data-hold (ms the full prompt shows before looping, default 2800).
    // While it expands the element has the class prx-busy, and prx-done after.
    // Script use: animacious.promptExpand(el, true) expands it, false shrinks it.
    var DEFAULTS = { delay: 1400, stagger: 70, color: '#7c3aed', hold: 2800 };

    var css =
      '.prx ins{text-decoration:none;color:inherit;background:none}' +
      '.prx-w{display:inline-block;vertical-align:bottom;overflow:hidden;white-space:pre;max-width:0;opacity:0;border-radius:.25em;' +
      'background:color-mix(in srgb,var(--prx-c) 0%,transparent)}' +
      '.prx-w>span{display:inline-block}' +
      '.prx-fresh{background:color-mix(in srgb,var(--prx-c) 16%,transparent);color:color-mix(in srgb,var(--prx-c) 75%,currentColor)}' +
      '.prx-w.prx-settle{transition:background-color 1.2s ease,color 1.2s ease}';
    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) {
      el.classList.add('prx');
      el.style.setProperty('--prx-c', el.dataset.color || DEFAULTS.color);
      var words = [];
      // Each added word (with the space in front of it) becomes its own clipped
      // box, so it can open from nothing and push the text along.
      [].slice.call(el.querySelectorAll('ins')).forEach(function (ins) {
        var text = ins.textContent;
        ins.textContent = '';
        // Keep the space between the old words inside the new part, so the short
        // prompt doesn't show a double space where the new words will go.
        var prev = ins.previousSibling, next = ins.nextSibling;
        if (prev && prev.nodeType === 3 && /\s$/.test(prev.textContent) && !/^\s/.test(text)) {
          prev.textContent = prev.textContent.replace(/\s+$/, '');
          text = ' ' + text;
        } else if ((!prev || (prev.nodeType === 3 && !prev.textContent.trim() && !prev.previousSibling)) &&
            next && next.nodeType === 3 && /^\s/.test(next.textContent) && !/\s$/.test(text)) {
          next.textContent = next.textContent.replace(/^\s+/, '');
          text = text + ' ';
        }
        (text.match(/\s*\S+|\s+/g) || []).forEach(function (chunk) {
          var w = document.createElement('span');
          w.className = 'prx-w';
          var inner = document.createElement('span');
          inner.textContent = chunk;
          w.appendChild(inner);
          ins.appendChild(w);
          words.push(w);
        });
      });
      el._prx = { words: words, open: false, run: 0, timers: [], anims: [] };
    }

    function stop(st) {
      st.run++;
      st.timers.forEach(clearTimeout);
      st.anims.forEach(function (a) { a.pause(); });
      st.timers = [];
      st.anims = [];
    }

    // Once open, the words go back to being plain inline text so they wrap normally.
    function inline(w) {
      w.style.maxWidth = 'none';
      w.style.whiteSpace = 'normal';
      w.style.display = 'inline';
      w.firstChild.style.display = 'inline';
    }

    function full(el) {
      el._prx.words.forEach(function (w) { inline(w); w.style.opacity = 1; });
      el._prx.open = true;
      el.classList.add('prx-done');
    }

    function expand(el, onDone) {
      var st = el._prx, d = el.dataset;
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      stop(st);
      var run = st.run;
      el.classList.add('prx-busy');
      el.classList.remove('prx-done');
      st.words.forEach(function (w, i) {
        // Measure the word's natural width, then open its box up to it.
        var inner = w.firstChild;
        w.classList.remove('prx-settle');
        w.classList.add('prx-fresh');
        var target = inner.getBoundingClientRect().width + 1;
        var delay = i * stagger;
        st.anims.push(anime.animate(w, { maxWidth: [0, target], opacity: [0, 1], duration: 520, delay: delay, ease: 'out(3)' }));
        st.anims.push(anime.animate(inner, { y: ['0.7em', 0], filter: ['blur(4px)', 'blur(0px)'], duration: 560, delay: delay + 60, ease: 'out(3)' }));
        st.timers.push(setTimeout(function () {
          if (run !== st.run) return;
          // Settled: drop the highlight and let the word wrap like normal text.
          w.classList.add('prx-settle');
          w.classList.remove('prx-fresh');
        }, delay + 1100));
      });
      var total = (st.words.length - 1) * stagger + 1100;
      st.timers.push(setTimeout(function () {
        if (run !== st.run) return;
        st.words.forEach(inline);
        st.open = true;
        el.classList.remove('prx-busy');
        el.classList.add('prx-done');
        if (onDone) onDone();
      }, total + 300));
    }

    function shrink(el, onDone) {
      var st = el._prx;
      stop(st);
      var run = st.run;
      el.classList.remove('prx-done', 'prx-busy');
      var n = st.words.length;
      st.words.forEach(function (w, i) {
        w.classList.remove('prx-fresh', 'prx-settle');
        w.style.display = '';
        w.style.whiteSpace = '';
        w.firstChild.style.display = '';
        var width = w.firstChild.getBoundingClientRect().width + 1;
        w.style.maxWidth = width + 'px';
        st.anims.push(anime.animate(w, { maxWidth: [width, 0], opacity: [1, 0], duration: 420, delay: (n - 1 - i) * 18, ease: 'inOut(2)' }));
      });
      st.timers.push(setTimeout(function () {
        if (run !== st.run) return;
        st.open = false;
        if (onDone) onDone();
      }, (n - 1) * 18 + 500));
    }

    function cycle(el) {
      var st = el._prx, d = el.dataset;
      var delay = d.delay !== undefined ? +d.delay : DEFAULTS.delay;
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      function wait(fn, ms) { var r = st.run; st.timers.push(setTimeout(function () { if (r === st.run) fn(); }, ms)); }
      wait(function () {
        expand(el, function () {
          if (d.loop !== undefined) wait(function () { shrink(el, function () { cycle(el); }); }, hold);
        });
      }, delay);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          cycle(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-prompt-expand]').forEach(function (el) {
        if (el._prx) return;
        setup(el);
        if (reduced()) full(el);
        else io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.promptExpand = function (el, open) {
      if (!el || !el._prx) return;
      if (open === undefined) open = !el._prx.open;
      if (reduced()) { if (open) full(el); return; }
      if (open) expand(el); else shrink(el);
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p data-prompt-expand>
  A <ins>fluffy ginger</ins> cat <ins>curled up</ins> on a windowsill<ins>,
  soft morning light, film photo</ins>
</p>