← AI

Tool Call Chip

A little pill shows an AI tool at work: a spinner turns, a soft fill sweeps across the chip with a shine running through it, and a timer counts the seconds. When the tool is done the spinner pops into a green tick and the label changes to say what happened. Put it on a list and the chips run one after another, like an agent working through its tools.

What will the weather be like in Lisbon on Saturday?

  • Searching the web
  • Reading forecasts
  • Averaging the highs

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-tool-call-chip to a <span> holding what the tool is doing, with data-done for the finished label.
  3. Or put it on a <ul>: each <li> becomes a chip and they run in turn. Items can have their own data-done, data-icon and data-duration.

Options

Add any of these attributes to the same element.

data-doneLabel once the tool has finished. Default the same label.
data-iconsearch, globe, code, file, calc, image or none. Default search.
data-durationHow long the tool runs. Default 2400 ms.
data-gapPause between chips in a list. Default 250 ms.
data-timer="false"Hide the seconds counter.
data-colorColour of the fill and spinner. Default #7c3aed.
data-loopRun through again after a pause.
data-holdHow long the finished chips show before looping. Default 2400 ms.

To match a real tool call, leave out data-loop, set data-duration to roughly how long it takes, and call animacious.toolCallChip(el) each time the tool runs.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Tool Call Chip: a little pill that shows an AI tool at work. A spinner
    // turns, a soft fill sweeps across the chip and a timer counts up; when the
    // tool is done the spinner becomes a tick and the label changes to say so.
    // Put it on a list and each item runs in turn, like an agent's tool calls.
    // Usage: <span data-tool-call-chip data-done="Searched the web">Searching the web</span>
    //   or:  <ul data-tool-call-chip><li data-icon="file" data-done="Read 3 files">Reading files</li>…</ul>
    // Optional: data-done (label when finished, default the same label),
    // data-icon (search, globe, code, file, calc, image or none; default search),
    // data-duration (ms the tool runs, default 2400), data-gap (ms between list
    // items, default 250), data-timer="false" (hide the seconds), data-color
    // (fill and spinner colour, default #7c3aed), data-loop (run again),
    // data-hold (ms the finished chips show before looping, default 2400).
    // List items can set their own data-done, data-icon and data-duration.
    // Script use: animacious.toolCallChip(el) runs it again.
    var DEFAULTS = { icon: 'search', duration: 2400, gap: 250, color: '#7c3aed', hold: 2400 };

    var css =
      '.tcc-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;align-items:flex-start;gap:.55em}' +
      '.tcc-chip{position:relative;display:inline-flex;align-items:center;gap:.5em;max-width:100%;padding:.42em .85em .42em .6em;' +
      'border-radius:999px;overflow:hidden;isolation:isolate;line-height:1.2;white-space:nowrap;text-align:left;' +
      'background:color-mix(in srgb,var(--tcc-c) 7%,transparent);border:1px solid color-mix(in srgb,var(--tcc-c) 22%,transparent);' +
      'transition:border-color .4s,background-color .4s}' +
      '.tcc-fill{position:absolute;inset:0;z-index:-1;transform-origin:0 50%;transform:scaleX(0);' +
      'background:linear-gradient(90deg,color-mix(in srgb,var(--tcc-c) 6%,transparent),color-mix(in srgb,var(--tcc-c) 20%,transparent))}' +
      '.tcc-fill::after{content:"";position:absolute;inset:0;opacity:0;background:linear-gradient(100deg,transparent 30%,' +
      'color-mix(in srgb,#fff 55%,transparent) 50%,transparent 70%);background-size:250% 100%}' +
      '.tcc-running .tcc-fill::after{opacity:1;animation:tcc-sweep 1.3s linear infinite}' +
      '.tcc-icon{position:relative;flex:none;width:1.05em;height:1.05em}' +
      '.tcc-spin{position:absolute;inset:.06em;border-radius:50%;border:.14em solid color-mix(in srgb,var(--tcc-c) 25%,transparent);' +
      'border-top-color:var(--tcc-c);animation:tcc-spin .75s linear infinite}' +
      '.tcc-tick{position:absolute;inset:0;width:100%;height:100%;border-radius:50%;background:var(--tcc-ok);color:#fff;transform:scale(0)}' +
      '.tcc-tool{flex:none;width:1em;height:1em;opacity:.7}' +
      '.tcc-label{display:inline-grid;min-width:0}' +
      '.tcc-label>span{grid-area:1/1;overflow:hidden;text-overflow:ellipsis}' +
      '.tcc-done-l{opacity:0}' +
      '.tcc-dots::after{content:"";display:inline-block;width:1.1em;text-align:left;animation:tcc-dots 1.2s steps(4) infinite}' +
      '.tcc-time{flex:none;font-variant-numeric:tabular-nums;font-size:.85em;opacity:.55;min-width:2.6em;text-align:right}' +
      '.tcc-ok{border-color:color-mix(in srgb,var(--tcc-ok) 35%,transparent)}' +
      '.tcc-ok .tcc-spin{display:none}' +
      '@keyframes tcc-spin{to{transform:rotate(360deg)}}' +
      '@keyframes tcc-sweep{from{background-position:120% 0}to{background-position:-20% 0}}' +
      '@keyframes tcc-dots{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function svg(d) {
      return '<svg class="tcc-tool" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" ' +
        'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="' + d + '"/></svg>';
    }
    var ICONS = {
      search: svg('M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13zM15.5 15.5L20 20'),
      globe: svg('M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M12 3c2.5 2.6 3.7 5.6 3.7 9s-1.2 6.4-3.7 9c-2.5-2.6-3.7-5.6-3.7-9S9.5 5.6 12 3z'),
      code: svg('M8.5 7L3.5 12l5 5M15.5 7l5 5-5 5M13.5 4.5l-3 15'),
      file: svg('M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8zM14 3v5h5M9 13h6M9 17h4'),
      calc: svg('M7 3h10a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.5 7h7M8.5 12h.01M12 12h.01M15.5 12h.01M8.5 16h.01M12 16h.01M15.5 16h.01'),
      image: svg('M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 20')
    };
    var TICK = '<svg class="tcc-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.2" ' +
      'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 12.5l3.2 3.2L17 9"/></svg>';

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

    function opt(chip, el, key) {
      var v = chip.dataset[key];
      if (v === undefined) v = el.dataset[key];
      return v === undefined ? DEFAULTS[key] : v;
    }

    function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;'); }

    function build(chip, el) {
      var label = chip.textContent.trim().replace(/(\.\.\.|…)$/, '');
      var done = opt(chip, el, 'done') || label;
      var icon = ICONS[opt(chip, el, 'icon')] || '';
      var timer = el.dataset.timer !== 'false';
      chip.classList.add('tcc-chip');
      chip.setAttribute('role', 'status');
      chip.innerHTML =
        '<span class="tcc-fill" aria-hidden="true"></span>' +
        '<span class="tcc-icon" aria-hidden="true"><span class="tcc-spin"></span>' + TICK + '</span>' + icon +
        '<span class="tcc-label"><span class="tcc-run-l tcc-dots">' + esc(label) + '</span>' +
        '<span class="tcc-done-l">' + esc(done) + '</span></span>' +
        (timer ? '<span class="tcc-time" aria-hidden="true">0.0s</span>' : '');
      return {
        el: chip, duration: +opt(chip, el, 'duration') || DEFAULTS.duration,
        fill: chip.querySelector('.tcc-fill'), tick: chip.querySelector('.tcc-tick'),
        run: chip.querySelector('.tcc-run-l'), done: chip.querySelector('.tcc-done-l'),
        time: chip.querySelector('.tcc-time'), anims: []
      };
    }

    function setup(el) {
      var list = el.tagName === 'UL' || el.tagName === 'OL';
      var color = el.dataset.color || DEFAULTS.color;
      el.style.setProperty('--tcc-c', color);
      el.style.setProperty('--tcc-ok', '#16a34a');
      if (list) el.classList.add('tcc-list');
      var nodes = list ? [].slice.call(el.children) : [el];
      el._tcc = { chips: nodes.map(function (n) { return build(n, el); }), timers: [], run: 0 };
    }

    function reset(c) {
      c.anims.forEach(function (a) { a.pause(); });
      c.anims = [];
      c.el.classList.remove('tcc-ok', 'tcc-running');
      anime.utils.set(c.fill, { scaleX: 0, opacity: 1 });
      anime.utils.set(c.tick, { scale: 0 });
      anime.utils.set(c.run, { opacity: 1, y: 0 });
      anime.utils.set(c.done, { opacity: 0, y: 0 });
      if (c.time) c.time.textContent = '0.0s';
    }

    function finished(c, secs) {
      c.el.classList.add('tcc-ok');
      c.el.classList.remove('tcc-running');
      anime.utils.set(c.fill, { scaleX: 1 });
      anime.utils.set(c.tick, { scale: 1 });
      anime.utils.set(c.run, { opacity: 0 });
      anime.utils.set(c.done, { opacity: 1 });
      if (c.time) c.time.textContent = secs.toFixed(1) + 's';
    }

    function runChip(c, onDone) {
      reset(c);
      c.el.classList.add('tcc-running');
      var start = performance.now();
      // The fill grows across the chip while the tool runs; a timer keeps the clock.
      c.anims.push(anime.animate(c.fill, { scaleX: [0, 1], duration: c.duration, ease: 'out(2)' }));
      var tick = anime.createTimer({
        duration: c.duration,
        onUpdate: function () { if (c.time) c.time.textContent = ((performance.now() - start) / 1000).toFixed(1) + 's'; },
        onComplete: function () {
          if (c.time) c.time.textContent = (c.duration / 1000).toFixed(1) + 's';
          c.el.classList.remove('tcc-running');
          c.el.classList.add('tcc-ok');
          c.anims.push(anime.animate(c.tick, { scale: [0, 1], rotate: [-90, 0], duration: 600, ease: 'outBack(2)' }));
          c.anims.push(anime.animate(c.fill, { opacity: [1, 0.35], duration: 700, ease: 'out(2)' }));
          c.anims.push(anime.animate(c.run, { opacity: 0, y: -8, duration: 260, ease: 'in(2)' }));
          c.anims.push(anime.animate(c.done, { opacity: [0, 1], y: [8, 0], duration: 380, delay: 120, ease: 'out(3)' }));
          c.anims.push(anime.animate(c.el, { scale: [1, 1.04, 1], duration: 420, ease: 'out(2)' }));
          if (onDone) onDone();
        }
      });
      c.anims.push(tick);
    }

    function play(el) {
      var st = el._tcc, d = el.dataset;
      var gap = d.gap !== undefined ? +d.gap : DEFAULTS.gap;
      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.chips.forEach(function (c) { reset(c); if (st.chips.length > 1) anime.utils.set(c.el, { opacity: 0, y: 6 }); });
      function next(i) {
        if (i >= st.chips.length) {
          if (d.loop !== undefined) later(function () {
            anime.animate(st.chips.map(function (c) { return c.el; }), {
              opacity: 0, y: -4, duration: 350, delay: anime.stagger(60), ease: 'in(2)',
              onComplete: function () { if (run === st.run) later(function () { play(el); }, 250); }
            });
          }, hold);
          return;
        }
        var c = st.chips[i];
        if (st.chips.length > 1) anime.animate(c.el, { opacity: [0, 1], y: [6, 0], duration: 380, ease: 'out(3)' });
        else anime.utils.set(c.el, { opacity: 1, y: 0 });
        runChip(c, function () { later(function () { next(i + 1); }, gap); });
      }
      next(0);
    }

    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-tool-call-chip]').forEach(function (el) {
        if (el._tcc) return;
        setup(el);
        if (reduced()) el._tcc.chips.forEach(function (c) { finished(c, c.duration / 1000); });
        else io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.toolCallChip = function (el) { if (el && el._tcc) play(el); };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<span data-tool-call-chip data-done="Searched the web">Searching the web</span>

<!-- several tools in turn: -->
<ul data-tool-call-chip>
  <li data-done="Searched the web">Searching the web</li>
  <li data-icon="file" data-done="Read 4 pages">Reading pages</li>
  <li data-icon="code" data-done="Ran the code">Running code</li>
</ul>