← AI

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.

  1. Reading the question
  2. Checking train times for Saturday
  3. Comparing three routes
  4. Picking the quickest one

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-reasoning-steps-fold to a box holding a list (<ol> or <ul>) of steps.
  3. 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-stepHow long each step takes. Default 1200 ms.
data-summarySummary text: {s} becomes the seconds and {n} the number of steps. Default “Thought for {s}s · {n} steps”.
data-thinkingHeading while it works. Default “Thinking…”.
data-loopRun through again after a pause.
data-holdHow long the summary shows before looping. Default 3000 ms.
1 · Snippet
<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>
2 · Your HTML
<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>