← AI

Document Scan Extract

A glowing scan line sweeps down a document. As it passes each field you’ve marked, the field lights up inside a box and its value drops into a list of extracted fields beside the page, with a tick, the way AI document readers pull out names, dates and totals.

INVOICE#INV-2041
Northwind Ltd 12 Mar 2026
£1,284.00

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-document-scan-extract to a box whose first child is the document.
  3. Mark each value to pull out with data-field="Label". The list of fields is added after the document, so lay the box out as a row or grid (see the usage).

Options

Add any of these attributes to the same element.

data-fieldOn each value in the document: its label in the list.
data-durationHow long the scan takes. Default 2600 ms.
data-colorColour of the scan line, boxes and ticks. Default #7c3aed.
data-titleHeading over the list. Default “Extracted”; set it to empty for none.
data-loopScan again after a pause.
data-holdHow long the finished result shows before looping. Default 2600 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Document Scan Extract: a glowing scan line sweeps down a document. As it
    // passes each marked field, the field lights up with a box and its value
    // drops into a list of extracted fields beside the page, with a tick.
    // Usage: <div data-document-scan-extract>
    //          <div class="my-invoice">… <span data-field="Total">£1,284.00</span> …</div>
    //        </div>
    // The first child is the document; anything inside it with data-field is
    // extracted, labelled with the attribute's value. The list of fields is
    // added after the document; style the container (a grid or flex row) to
    // lay the two out.
    // Optional: data-duration (ms the scan takes, default 2600), data-loop
    // (scan again), data-hold (ms the finished result shows before looping,
    // default 2600), data-color (scan and highlight colour, default #7c3aed),
    // data-title (heading over the list, default "Extracted"; "" for none).
    var DEFAULTS = { duration: 2600, hold: 2600, color: '#7c3aed', title: 'Extracted' };

    var css =
      '.dse-doc{position:relative;overflow:hidden}' +
      '.dse-line{position:absolute;left:0;right:0;top:0;height:2px;z-index:3;pointer-events:none;opacity:0;' +
      'background:var(--dse-c);box-shadow:0 0 8px 1px var(--dse-c)}' +
      '.dse-line::before{content:"";position:absolute;left:0;right:0;bottom:2px;height:3.5em;' +
      'background:linear-gradient(to top,color-mix(in srgb,var(--dse-c) 22%,transparent),transparent)}' +
      '.dse-f{position:relative;border-radius:3px;transition:color .3s}' +
      '.dse-f::after{content:"";position:absolute;inset:-2px -4px;border-radius:4px;border:1.5px solid var(--dse-c);' +
      'background:color-mix(in srgb,var(--dse-c) 12%,transparent);opacity:0;transform:scale(1.25);transition:opacity .3s,transform .4s cubic-bezier(.2,1.4,.4,1)}' +
      '.dse-hit::after{opacity:1;transform:none}' +
      '.dse-out{display:flex;flex-direction:column;gap:.45em;min-width:0;text-align:left}' +
      '.dse-head{font-size:.75em;font-weight:700;letter-spacing:.06em;text-transform:uppercase;opacity:.55;margin-bottom:.1em}' +
      '.dse-row{display:grid;grid-template-columns:1.15em minmax(0,1fr);column-gap:.5em;align-items:center;padding:.45em .6em;border-radius:.6em;' +
      'background:color-mix(in srgb,currentColor 5%,transparent);border:1px solid color-mix(in srgb,currentColor 10%,transparent);transition:border-color .3s,background-color .3s}' +
      '.dse-row.dse-got{border-color:color-mix(in srgb,var(--dse-c) 40%,transparent);background:color-mix(in srgb,var(--dse-c) 7%,transparent)}' +
      '.dse-k{grid-column:2;font-size:.72em;opacity:.6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
      '.dse-v{grid-column:2;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:1.25em;position:relative}' +
      '.dse-v:empty::before{content:"";position:absolute;left:0;top:.3em;width:70%;height:.7em;border-radius:.35em;' +
      'background:color-mix(in srgb,currentColor 12%,transparent)}' +
      '.dse-ck{grid-row:1/3;width:1.15em;height:1.15em;border-radius:50%;border:1.5px solid color-mix(in srgb,currentColor 25%,transparent);' +
      'display:grid;place-items:center;color:#fff;transition:background-color .3s,border-color .3s}' +
      '.dse-ck svg{width:72%;height:72%;opacity:0;transition:opacity .2s}' +
      '.dse-got .dse-ck{background:var(--dse-c);border-color:var(--dse-c)}' +
      '.dse-got .dse-ck svg{opacity:1}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var TICK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" ' +
      'stroke-linejoin="round" aria-hidden="true"><path d="M6 12.5l4 4L18 8"/></svg>';

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

    function setup(el) {
      var d = el.dataset;
      var doc = el.firstElementChild;
      if (!doc) return false;
      el.style.setProperty('--dse-c', d.color || DEFAULTS.color);
      doc.classList.add('dse-doc');
      var line = document.createElement('div');
      line.className = 'dse-line';
      line.setAttribute('aria-hidden', 'true');
      doc.appendChild(line);
      var out = document.createElement('div');
      out.className = 'dse-out';
      var title = d.title !== undefined ? d.title : DEFAULTS.title;
      if (title) {
        var hd = document.createElement('div');
        hd.className = 'dse-head';
        hd.textContent = title;
        out.appendChild(hd);
      }
      var fields = [].slice.call(doc.querySelectorAll('[data-field]')).map(function (f) {
        f.classList.add('dse-f');
        var row = document.createElement('div');
        row.className = 'dse-row';
        row.innerHTML = '<span class="dse-ck" aria-hidden="true">' + TICK + '</span><span class="dse-k"></span><span class="dse-v"></span>';
        row.querySelector('.dse-k').textContent = f.dataset.field;
        out.appendChild(row);
        return { f: f, row: row, v: row.querySelector('.dse-v'), value: f.textContent.trim() };
      });
      el.appendChild(out);
      el._dse = { doc: doc, line: line, fields: fields, timers: [], run: 0, anim: null };
      return true;
    }

    function fill(fd) {
      fd.f.classList.add('dse-hit');
      fd.row.classList.add('dse-got');
      fd.v.textContent = fd.value;
    }

    function play(el) {
      var st = el._dse, d = el.dataset;
      var dur = +d.duration || DEFAULTS.duration;
      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.fields.forEach(function (fd) { fd.f.classList.remove('dse-hit'); fd.row.classList.remove('dse-got'); fd.v.textContent = ''; });
      var H = st.doc.clientHeight;
      var docTop = st.doc.getBoundingClientRect().top;
      // When the line reaches the middle of each field, that field is caught.
      st.fields.forEach(function (fd) {
        var r = fd.f.getBoundingClientRect();
        var at = Math.max(0, Math.min(1, (r.top + r.height / 2 - docTop) / H));
        later(function () {
          fill(fd);
          anime.animate(fd.v, { opacity: [0, 1], x: [-10, 0], duration: 450, ease: 'out(3)' });
          anime.animate(fd.row, { scale: [0.97, 1], duration: 450, ease: 'outBack(2)' });
        }, 250 + at * dur);
      });
      anime.utils.set(st.line, { y: 0, opacity: 0 });
      anime.animate(st.line, { opacity: [0, 1], duration: 250, ease: 'out(2)' });
      st.anim = anime.animate(st.line, { y: [0, H - 2], duration: dur, delay: 250, ease: 'linear' });
      later(function () { anime.animate(st.line, { opacity: 0, duration: 350, ease: 'in(2)' }); }, 250 + dur);
      if (d.loop !== undefined) later(function () { play(el); }, 250 + dur + hold);
    }

    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-document-scan-extract]').forEach(function (el) {
        if (el._dse) return;
        if (!setup(el)) return;
        if (reduced()) el._dse.fields.forEach(fill);
        else io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-document-scan-extract style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px">
  <div class="invoice">
    <h3>Invoice</h3>
    <p>From <span data-field="Vendor">Northwind Ltd</span></p>
    <p>Dated <span data-field="Date">12 Mar 2026</span></p>
    <p>Total due <span data-field="Total">£1,284.00</span></p>
  </div>
</div>