← AI

Summary Collapse

A long passage squeezes down into a short summary. The key phrases light up one by one, the rest of the words blur away, the box closes up to a couple of lines and the summary rises into place, while a little label goes from “Original · 64 words” to “Summary · 17 words”.

After a long discussion at Thursday’s meeting, and plenty of back and forth about rotas, cover for holidays and what our customers might expect, the team agreed that we would move to a four-day week for everyone. The change starts at the beginning of June, and we’ll treat it as a three-month trial, checking in every few weeks, before deciding whether to keep it.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-summary-collapse to a box holding the long text, with the summary in data-summary.
  3. Wrap the key phrases in <mark> to have them light up before the rest fades.

Options

Add any of these attributes to the same element.

data-summaryThe short version.
data-delayHow long the full text shows before it’s summarised. Default 1600 ms.
data-loopOpen back up to the full text and summarise it again.
data-holdHow long the summary shows before looping. Default 3000 ms.
data-colorColour of the label and highlights. Default #7c3aed.
data-label="false"Hide the word-count label.

Call animacious.summaryCollapse(el, true) to summarise from your own button, and false to show the full text again.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Summary Collapse: a long passage squeezes down into a short summary. The
    // key phrases light up first, the rest of the words fade away, the box
    // closes up to a few lines and the summary rises into place. A small label
    // above counts the words before and after.
    // Usage: <div data-summary-collapse data-summary="The short version.">
    //          <p>The long version, with the <mark>key phrases</mark> marked…</p>
    //        </div>
    // Optional: data-summary (the summary text), data-loop (open back up to
    // the full text and summarise again), data-delay (ms the full text shows
    // first, default 1600), data-hold (ms the summary shows, default 3000),
    // data-color (highlight colour, default #7c3aed), data-label="false" (no
    // word-count label).
    // Script use: animacious.summaryCollapse(el, true) to summarise, false to
    // open it back up.
    var DEFAULTS = { delay: 1600, hold: 3000, color: '#7c3aed' };

    var css =
      '.smc-label{display:flex;align-items:center;gap:.45em;margin-bottom:.6em;font-size:.75em;font-weight:650;letter-spacing:.02em;' +
      'color:var(--smc-c);white-space:nowrap}' +
      '.smc-label svg{flex:none;width:1.1em;height:1.1em}' +
      '.smc-label span{position:relative;display:inline-grid;overflow:hidden}' +
      '.smc-label span>i{grid-area:1/1;font-style:normal}' +
      '.smc-busy .smc-label i{background:linear-gradient(100deg,color-mix(in srgb,var(--smc-c) 45%,transparent) 0 38%,var(--smc-c) 50%,' +
      'color-mix(in srgb,var(--smc-c) 45%,transparent) 62%);background-size:260% 100%;-webkit-background-clip:text;background-clip:text;' +
      '-webkit-text-fill-color:transparent;animation:smc-sweep 1.4s linear infinite}' +
      '.smc-body{position:relative;overflow:hidden}' +
      '.smc-long,.smc-short{position:absolute;left:0;right:0;top:0}' +
      '.smc-long>:first-child,.smc-short>:first-child{margin-top:0}' +
      '.smc-long>:last-child,.smc-short>:last-child{margin-bottom:0}' +
      '.smc-long mark{color:inherit;background:none;border-radius:.2em;box-shadow:0 0 0 .1em transparent;transition:background-color .4s,box-shadow .4s}' +
      '.smc-long mark.smc-on{background:color-mix(in srgb,var(--smc-c) 18%,transparent);box-shadow:0 0 0 .1em color-mix(in srgb,var(--smc-c) 18%,transparent)}' +
      '.smc-short{opacity:0}' +
      '.smc-w{display:inline-block}' +
      '.smc-glue{white-space:nowrap}.smc-glue>:first-child{white-space:normal}' +
      '@keyframes smc-sweep{from{background-position:100% 0}to{background-position:0 0}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var SPARK = '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 2c.5 4.6 2.1 7.2 3.7 8.2 1.5 1 3.6 1.4 6.3 1.8-2.7.4-4.8.8-6.3 1.8-1.6 1-3.2 3.6-3.7 8.2-.5-4.6-2.1-7.2-3.7-8.2C6.8 12.8 4.7 12.4 2 12c2.7-.4 4.8-.8 6.3-1.8C9.9 9.2 11.5 6.6 12 2z"/></svg>';

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

    // Wrap each word outside <mark> in a span so the words can fade one by one.
    function wrapWords(root, list) {
      [].slice.call(root.childNodes).forEach(function (n) {
        if (n.nodeType === 3) {
          var parts = n.textContent.split(/(\s+)/);
          var frag = document.createDocumentFragment();
          var before = n.previousSibling;
          parts.forEach(function (p, i) {
            if (!p) return;
            if (/^\s+$/.test(p)) { frag.appendChild(document.createTextNode(p)); return; }
            var s = document.createElement('span');
            s.className = 'smc-w';
            s.textContent = p;
            list.push(s);
            // Punctuation straight after a <mark> stays on its line.
            if (i === 0 && before && before.nodeType === 1) {
              var glue = document.createElement('span');
              glue.className = 'smc-glue';
              before.parentNode.insertBefore(glue, before);
              glue.appendChild(before);
              glue.appendChild(s);
              return;
            }
            frag.appendChild(s);
          });
          n.parentNode.replaceChild(frag, n);
        } else if (n.nodeType === 1 && n.tagName !== 'MARK') wrapWords(n, list);
      });
    }

    function count(text) { return (text.trim().match(/\S+/g) || []).length; }

    function setup(el) {
      var d = el.dataset;
      el.style.setProperty('--smc-c', d.color || DEFAULTS.color);
      var long = document.createElement('div');
      long.className = 'smc-long';
      while (el.firstChild) long.appendChild(el.firstChild);
      var nLong = count(long.textContent);
      var short = document.createElement('div');
      short.className = 'smc-short';
      var p = document.createElement('p');
      p.textContent = d.summary || '';
      short.appendChild(p);
      var nShort = count(p.textContent);
      var words = [];
      wrapWords(long, words);
      var sWords = [];
      wrapWords(p, sWords);
      var body = document.createElement('div');
      body.className = 'smc-body';
      body.appendChild(long);
      body.appendChild(short);
      var st = el._smc = {
        body: body, long: long, short: short, words: words, sWords: sWords, marks: [].slice.call(long.querySelectorAll('mark')),
        label: null, texts: ['Original · ' + nLong + ' words', 'Summarising…', 'Summary · ' + nShort + ' words'],
        state: false, busy: false, timers: [], run: 0
      };
      if (d.label !== 'false') {
        var label = document.createElement('div');
        label.className = 'smc-label';
        label.setAttribute('aria-hidden', 'true');
        label.innerHTML = SPARK + '<span><i></i></span>';
        el.appendChild(label);
        st.label = label.querySelector('span');
        st.label.firstChild.textContent = st.texts[0];
      }
      el.appendChild(body);
      long.setAttribute('aria-hidden', 'false');
      short.setAttribute('aria-hidden', 'true');
      fit(el);
      new ResizeObserver(function () { if (!st.busy) fit(el); }).observe(el);
    }

    function fit(el) {
      var st = el._smc;
      st.body.style.height = (st.state ? st.short : st.long).offsetHeight + 'px';
    }

    function label(el, i) {
      var st = el._smc;
      if (!st.label) return;
      var old = st.label.lastElementChild;
      if (old.textContent === st.texts[i]) return;
      var s = document.createElement('i');
      s.textContent = st.texts[i];
      st.label.appendChild(s);
      if (reduced()) { old.remove(); return; }
      anime.animate(old, { y: ['0%', '-100%'], opacity: 0, duration: 300, ease: 'in(2)', onComplete: function () { old.remove(); } });
      anime.animate(s, { y: ['100%', '0%'], opacity: [0, 1], duration: 380, delay: 80, ease: 'out(3)' });
    }

    function set(el, summary, done) {
      var st = el._smc;
      var run = ++st.run;
      st.timers.forEach(clearTimeout);
      st.timers = [];
      function later(fn, ms) { st.timers.push(setTimeout(function () { if (run === st.run) fn(); }, ms)); }
      st.state = summary;
      st.long.setAttribute('aria-hidden', String(summary));
      st.short.setAttribute('aria-hidden', String(!summary));
      var from = st.body.offsetHeight;
      var to = (summary ? st.short : st.long).offsetHeight;
      if (reduced()) {
        st.marks.forEach(function (m) { m.classList.toggle('smc-on', summary); });
        anime.utils.set(st.long, { opacity: summary ? 0 : 1 });
        anime.utils.set(st.short, { opacity: summary ? 1 : 0 });
        st.body.style.height = to + 'px';
        label(el, summary ? 2 : 0);
        if (done) done();
        return;
      }
      st.busy = true;
      if (summary) {
        el.classList.add('smc-busy');
        label(el, 1);
        // Key phrases light up, then the rest of the words fade away.
        st.marks.forEach(function (m, i) { later(function () { m.classList.add('smc-on'); }, 150 + i * 160); });
        var shuffled = st.words.slice().sort(function () { return Math.random() - 0.5; });
        later(function () {
          anime.animate(shuffled, { opacity: [1, 0.08], filter: ['blur(0px)', 'blur(2px)'], duration: 380, delay: anime.stagger(700 / Math.max(1, shuffled.length)), ease: 'out(2)' });
        }, 700);
        // The box squeezes down while the summary rises in.
        later(function () {
          anime.animate(st.long, { opacity: 0, scaleY: [1, 0.85], duration: 600, ease: 'inOut(2)' });
          anime.animate(st.body, { height: [from, to], duration: 750, ease: 'inOut(3)' });
          anime.utils.set(st.short, { opacity: 1 });
          anime.animate(st.sWords, { opacity: [0, 1], y: [10, 0], filter: ['blur(3px)', 'blur(0px)'], duration: 500, delay: anime.stagger(45, { start: 350 }), ease: 'out(3)' });
        }, 1700);
        later(function () {
          el.classList.remove('smc-busy');
          label(el, 2);
          st.busy = false;
          if (done) done();
        }, 2500 + st.sWords.length * 45);
      } else {
        label(el, 0);
        anime.animate(st.short, { opacity: 0, duration: 300, ease: 'in(2)' });
        st.marks.forEach(function (m) { m.classList.remove('smc-on'); });
        anime.utils.set(st.words, { opacity: 1, filter: 'blur(0px)' });
        anime.utils.set(st.long, { scaleY: 1 });
        anime.animate(st.body, { height: [from, to], duration: 700, delay: 150, ease: 'inOut(3)' });
        anime.animate(st.long, { opacity: [0, 1], duration: 600, delay: 250, ease: 'out(2)' });
        later(function () { st.busy = false; if (done) done(); }, 900);
      }
    }

    function cycle(el) {
      var st = el._smc, d = el.dataset;
      var delay = d.delay !== undefined ? +d.delay : DEFAULTS.delay;
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      st.timers.push(setTimeout(function () {
        set(el, true, function () {
          if (d.loop === undefined) return;
          st.timers.push(setTimeout(function () { set(el, false, 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-summary-collapse]').forEach(function (el) {
        if (el._smc) return;
        setup(el);
        if (reduced()) set(el, true);
        else io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.summaryCollapse = function (el, on) { if (el && el._smc) set(el, on !== false); };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-summary-collapse data-summary="We'll trial a four-day week from June.">
  <p>After a long chat about rotas and holiday cover, we agreed to <mark>trial</mark>
  a <mark>four-day week</mark>, starting <mark>from June</mark>.</p>
</div>