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
- Paste the snippet just before
</head>. - Add
data-summary-collapseto a box holding the long text, with the summary indata-summary. - 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-summary | The short version. |
data-delay | How long the full text shows before it’s summarised. Default 1600 ms. |
data-loop | Open back up to the full text and summarise it again. |
data-hold | How long the summary shows before looping. Default 3000 ms. |
data-color | Colour 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.
<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><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>