Reasoning Steps Fold
An AI’s reasoning steps appear one at a time under a shimmering “Thinking…” heading, each with a spinner that turns into a tick. When the last one is done the list folds up into a single summary line, “Thought for 6s · 4 steps”, which opens again on click.
- Reading the question
- Checking train times for Saturday
- Comparing three routes
- Picking the quickest one
How to use it
- Paste the snippet just before
</head>. - Add
data-reasoning-steps-foldto a box holding a list (<ol>or<ul>) of steps. - Once folded, the summary line is a button that shows and hides the steps.
Options
Add any of these attributes to the same element.
data-step | How long each step takes. Default 1200 ms. |
data-summary | Summary text: {s} becomes the seconds and {n} the number of steps. Default “Thought for {s}s · {n} steps”. |
data-thinking | Heading while it works. Default “Thinking…”. |
data-loop | Run through again after a pause. |
data-hold | How long the summary shows before looping. Default 3000 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Reasoning Steps Fold: an AI's reasoning steps appear one at a time, each
// with a spinner that turns into a tick, under a shimmering "Thinking…"
// heading. When the last step is done the list folds up into one summary
// line ("Thought for 6s · 4 steps"), which opens and closes again on click.
// Usage: <div data-reasoning-steps-fold><ol><li>Read the question</li>…</ol></div>
// Optional: data-step (ms each step takes, default 1200), data-summary
// (summary text; {s} becomes the seconds and {n} the number of steps,
// default "Thought for {s}s · {n} steps"), data-thinking (heading while it
// works, default "Thinking…"), data-loop (run through again after a pause),
// data-hold (ms the summary shows before looping, default 3000).
var DEFAULTS = { step: 1200, summary: 'Thought for {s}s · {n} steps', thinking: 'Thinking…', hold: 3000 };
var css =
'[data-reasoning-steps-fold]{text-align:left}' +
'.rsf-head{display:flex;align-items:center;gap:.55em;width:100%;padding:0;margin:0;border:0;background:none;' +
'font:inherit;font-weight:600;color:inherit;text-align:left;cursor:default}' +
'.rsf-ready .rsf-head{cursor:pointer}' +
'.rsf-title{position:relative;display:inline-grid;flex:1;min-width:0;overflow:hidden}' +
'.rsf-title>span{grid-area:1/1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.rsf-shine{background:linear-gradient(100deg,var(--rsf-dim) 0 38%,currentColor 50%,var(--rsf-dim) 62% 100%);' +
'background-size:260% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;' +
'animation:rsf-sweep 1.9s linear infinite}' +
'.rsf-chev{flex:none;width:1em;height:1em;opacity:0;transition:transform .35s ease,opacity .3s}' +
'.rsf-ready .rsf-chev{opacity:.6}' +
'.rsf-open .rsf-chev{transform:rotate(180deg)}' +
'.rsf-list{list-style:none;margin:0;padding:0;overflow:hidden}' +
'.rsf-item{display:flex;align-items:flex-start;gap:.6em;overflow:hidden;opacity:.85}' +
'.rsf-item>span:last-child{padding:.3em 0}' +
'.rsf-dot{flex:none;width:1em;height:1em;margin-top:.42em;position:relative}' +
'.rsf-dot::before{content:"";position:absolute;inset:.08em;border-radius:50%;border:2px solid currentColor;' +
'border-right-color:transparent;opacity:.6;animation:rsf-spin .8s linear infinite}' +
'.rsf-done .rsf-dot::before{animation:none;border:0;opacity:1;inset:0;' +
'background:currentColor;-webkit-mask:var(--rsf-tick) center/100% no-repeat;mask:var(--rsf-tick) center/100% no-repeat}' +
'.rsf-rail{border-left:2px solid color-mix(in srgb,currentColor 18%,transparent);margin:.5em 0 0 .45em;padding-left:.9em}' +
'@keyframes rsf-spin{to{transform:rotate(360deg)}}' +
'@keyframes rsf-sweep{from{background-position:100% 0}to{background-position:0 0}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var TICK = 'url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27%23000%27 stroke-width=%273%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M5 12.5l4.5 4.5L19 7.5%27/%3E%3C/svg%3E")';
var SPARK = '<svg viewBox="0 0 24 24" width="1em" height="1em" aria-hidden="true" style="flex:none"><path fill="currentColor" ' +
'd="M12 1.5c.5 4.8 2.2 7.6 3.9 8.6 1.6 1 3.8 1.5 6.6 1.9-2.8.4-5 .9-6.6 1.9-1.7 1-3.4 3.8-3.9 8.6-.5-4.8-2.2-7.6-3.9-8.6-1.6-1-3.8-1.5-6.6-1.9 2.8-.4 5-.9 6.6-1.9 1.7-1 3.4-3.8 3.9-8.6z"/></svg>';
var CHEV = '<svg class="rsf-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" ' +
'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var list = el.querySelector('ol, ul');
if (!list) return false;
var inert = !!el.closest('[aria-hidden="true"]');
el.style.setProperty('--rsf-dim', 'color-mix(in srgb, currentColor 40%, transparent)');
el.style.setProperty('--rsf-tick', TICK);
var head = document.createElement(inert ? 'div' : 'button');
if (!inert) head.type = 'button';
head.className = 'rsf-head';
head.innerHTML = SPARK + '<span class="rsf-title"><span></span></span>' + CHEV;
var id = 'rsf-list-' + Math.random().toString(36).slice(2, 8);
list.id = list.id || id;
if (!inert) head.setAttribute('aria-controls', list.id);
var rail = document.createElement('div');
rail.className = 'rsf-rail';
list.classList.add('rsf-list');
list.parentNode.insertBefore(head, list);
list.parentNode.insertBefore(rail, list);
rail.appendChild(list);
var items = [].slice.call(list.children);
items.forEach(function (li) {
li.classList.add('rsf-item');
li.innerHTML = '<span class="rsf-dot" aria-hidden="true"></span><span>' + li.innerHTML + '</span>';
});
var st = { head: head, rail: rail, items: items, title: head.querySelector('.rsf-title'), timers: [], run: 0, open: true, ready: false };
el._rsf = st;
head.addEventListener('click', function () {
if (!st.ready) return;
setOpen(el, !st.open, true);
});
return true;
}
function title(el, text, shine, animate) {
var st = el._rsf;
var old = st.title.lastElementChild;
var s = document.createElement('span');
s.textContent = text;
if (shine) s.className = 'rsf-shine';
st.title.appendChild(s);
if (!animate) { if (old && old !== s) old.remove(); return; }
if (old) anime.animate(old, { y: ['0%', '-100%'], opacity: [1, 0], duration: 350, ease: 'in(2)', onComplete: function () { old.remove(); } });
anime.animate(s, { y: ['100%', '0%'], opacity: [0, 1], duration: 450, delay: 100, ease: 'out(3)' });
}
function setOpen(el, open, animate) {
var st = el._rsf;
st.open = open;
el.classList.toggle('rsf-open', open);
if (st.head.tagName === 'BUTTON') st.head.setAttribute('aria-expanded', String(open));
var rail = st.rail;
if (!animate) { rail.style.height = open ? '' : '0px'; rail.style.marginTop = open ? '' : '0px'; return; }
var from = rail.offsetHeight;
rail.style.height = '';
rail.style.marginTop = '';
var to = open ? rail.offsetHeight : 0;
var mt = parseFloat(getComputedStyle(rail).marginTop);
anime.animate(rail, {
height: [from, to], marginTop: open ? [0, mt] : [mt, 0], duration: 420, ease: 'inOut(3)',
onComplete: function () { if (st.open) { rail.style.height = ''; rail.style.marginTop = ''; } }
});
if (open) anime.animate(st.items, { opacity: [0, 0.85], y: [-6, 0], duration: 350, delay: anime.stagger(50) });
}
function finish(el, secs, animate) {
var st = el._rsf, d = el.dataset;
var text = (d.summary || DEFAULTS.summary).replace('{s}', secs).replace('{n}', st.items.length);
st.ready = true;
el.classList.add('rsf-ready');
title(el, text, false, animate);
if (!animate) { setOpen(el, false, false); return; }
// Fold: the steps slide up into the heading as the list closes.
anime.animate(st.items.slice().reverse(), { opacity: 0, y: -14, duration: 380, delay: anime.stagger(60), ease: 'in(2)' });
setTimeout(function () { setOpen(el, false, true); }, 200);
}
function play(el) {
var st = el._rsf, d = el.dataset;
var step = +d.step || DEFAULTS.step;
var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
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.ready = false;
el.classList.remove('rsf-ready');
st.open = true;
el.classList.add('rsf-open');
if (st.head.tagName === 'BUTTON') st.head.setAttribute('aria-expanded', 'true');
st.rail.style.height = ''; st.rail.style.marginTop = '';
st.items.forEach(function (li) {
li.classList.remove('rsf-done');
li.style.display = 'none';
anime.utils.set(li, { opacity: 0.85, y: 0 });
});
title(el, d.thinking || DEFAULTS.thinking, true, st.title.children.length > 0);
var start = Date.now();
st.items.forEach(function (li, i) {
later(function () {
if (i > 0) st.items[i - 1].classList.add('rsf-done');
li.style.display = '';
var hgt = li.offsetHeight;
anime.animate(li, {
height: [0, hgt], opacity: [0, 0.85], x: [-8, 0], duration: 420, ease: 'out(3)',
onComplete: function () { li.style.height = ''; }
});
}, 400 + i * step);
});
later(function () {
st.items[st.items.length - 1].classList.add('rsf-done');
}, 400 + st.items.length * step);
later(function () {
finish(el, Math.max(1, Math.round((Date.now() - start) / 1000)), true);
if (d.loop !== undefined) later(function () { play(el); }, hold + 800);
}, 400 + st.items.length * step + 700);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-reasoning-steps-fold]').forEach(function (el) {
if (el._rsf) return;
if (!setup(el)) return;
if (reduced()) {
el._rsf.items.forEach(function (li) { li.classList.add('rsf-done'); });
finish(el, Math.round(el._rsf.items.length * (+el.dataset.step || DEFAULTS.step) / 1000), false);
} else {
// Before it starts, show the first heading so the box isn't empty.
el._rsf.items.forEach(function (li) { li.style.display = 'none'; });
title(el, el.dataset.thinking || DEFAULTS.thinking, true, false);
io.observe(el);
}
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-reasoning-steps-fold>
<ol>
<li>Reading the question</li>
<li>Checking train times for Saturday</li>
<li>Comparing three routes</li>
<li>Picking the quickest one</li>
</ol>
</div>