← Illustrations

No Results Search

A magnifying glass sweeps back and forth over a row of empty result slots, magnifying each as it peers in. It finds nothing, stops in the middle, a question mark appears in the lens and it shakes its head.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-no-results-search> where the scene should go. It fills the element with an SVG scene.

Options

Add any of these attributes to the same element.

data-colorColour of the lens rim and question mark. Default #6a0dad.
data-durationTime for one search. Default 5200 ms.

It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion it shows the glass with its question mark.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // No Results Search: a magnifying glass sweeps back and forth over a set
    // of empty result slots, peering into each. It finds nothing, stops in
    // the middle, a question mark appears in the lens and it shakes its head.
    // For empty search results. Drawn as SVG that fills the element, and
    // paused when off screen.
    // Usage: <div data-no-results-search></div>
    // It is 8:5 by default on a transparent background.
    // Optional: data-color (lens rim colour, default #6a0dad),
    // data-duration (ms per search, default 5200).
    var DEFAULTS = { color: '#6a0dad', duration: 5200 };
    var uid = 0;

    var css =
      '[data-no-results-search]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.nrs-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}' +
      '.nrs-q{font:800 34px/1 system-ui,-apple-system,"Segoe UI",sans-serif}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
    function seg(t, a, b) { return clamp01((t - a) / (b - a)); }
    function inOut(k) { return k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2; }
    function outBack(k) { var c = 2.2; return 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); }

    function build(el) {
      var d = el.dataset;
      var id = 'nrs' + (++uid);
      var color = d.color || DEFAULTS.color;
      var cycle = +d.duration || DEFAULTS.duration;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A magnifying glass searching and finding nothing');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      // Empty result slots: dashed cards with faint placeholder lines.
      var slots = '';
      [92, 200, 308].forEach(function (x) {
        slots +=
          '<rect x="' + (x - 46) + '" y="112" width="92" height="96" rx="12" fill="#fff" fill-opacity=".55" stroke="#c9b8e3" stroke-width="2" stroke-dasharray="6 6"/>' +
          '<rect x="' + (x - 30) + '" y="132" width="60" height="26" rx="6" fill="#ede6f7"/>' +
          '<rect x="' + (x - 30) + '" y="168" width="44" height="6" rx="3" fill="#ede6f7"/>' +
          '<rect x="' + (x - 30) + '" y="182" width="30" height="6" rx="3" fill="#ede6f7"/>';
      });

      el.innerHTML =
        '<svg class="nrs-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs><clipPath id="' + id + '-lens"><circle r="27"/></clipPath></defs>' +
        '<g class="nrs-slots">' + slots + '</g>' +
        // The glass, drawn around the centre of its lens.
        '<g class="nrs-glass">' +
          '<line x1="26" y1="26" x2="54" y2="54" stroke="#1e1b2e" stroke-width="12" stroke-linecap="round"/>' +
          '<line x1="24" y1="24" x2="32" y2="32" stroke="' + color + '" stroke-width="14" stroke-linecap="butt"/>' +
          '<circle r="28" fill="#fff" fill-opacity=".72"/>' +
          '<g clip-path="url(#' + id + '-lens)"><g class="nrs-zoom"></g></g>' +
          '<circle r="28" fill="none" stroke="' + color + '" stroke-width="7"/>' +
          '<path d="M-17 -8A19 19 0 0 1 -6 -18" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity=".9"/>' +
          '<text class="nrs-q" x="0" y="12" text-anchor="middle" fill="' + color + '" opacity="0">?</text>' +
        '</g>' +
        '</svg>';

      var svg = el.firstChild;
      var glass = svg.querySelector('.nrs-glass');
      var q = svg.querySelector('.nrs-q');
      // The lens shows the slots magnified: a scaled copy, moved opposite the glass.
      var zoom = svg.querySelector('.nrs-zoom');
      zoom.innerHTML = slots;

      function place(x, y, rot, qk, shake) {
        glass.setAttribute('transform', 'translate(' + (x + shake).toFixed(2) + ' ' + y.toFixed(2) + ') rotate(' + rot.toFixed(2) + ')');
        // Undo the glass's rotation, then magnify around the lens centre.
        var Z = 1.7;
        zoom.setAttribute('transform', 'rotate(' + (-rot).toFixed(2) + ') scale(' + Z + ') translate(' + (-(x + shake)).toFixed(2) + ' ' + (-y).toFixed(2) + ')');
        q.setAttribute('opacity', qk.toFixed(3));
        q.setAttribute('transform', 'rotate(' + (-rot).toFixed(2) + ') scale(' + (0.4 + 0.6 * outBack(qk)).toFixed(3) + ')');
      }

      function frame(k) {
        var u = cycle / 5200, x, y, rot = 0, qk = 0, shake = 0;
        // Two sweeps: left to right, then back, dipping to peer into each slot.
        var s = seg(k, 0, 3000 * u);
        var path = s < 0.5 ? inOut(s * 2) : 1 - inOut((s - 0.5) * 2);
        x = 80 + path * 240;
        y = 150 - Math.abs(Math.sin(path * Math.PI * 2.5)) * 22;
        rot = (s < 0.5 ? 1 : -1) * 10 * Math.sin(Math.min(1, s < 0.5 ? s * 2 : (s - 0.5) * 2) * Math.PI);
        // Then to the middle, a question mark, and a shake of the head.
        var mid = inOut(seg(k, 3000 * u, 3600 * u));
        if (k > 3000 * u) { x = 80 + (200 - 80) * mid; y = 150 - 12 * mid; rot = 0; }
        qk = seg(k, 3500 * u, 3800 * u) * (1 - seg(k, 4800 * u, 5100 * u));
        var sh = seg(k, 3800 * u, 4600 * u);
        shake = sh > 0 && sh < 1 ? Math.sin(sh * Math.PI * 6) * 9 * (1 - sh) : 0;
        if (k > 4800 * u) { var back = inOut(seg(k, 4800 * u, 5200 * u)); x = 200 - 120 * back; y = 138 + 12 * back; }
        place(x, y, rot, qk, shake);
      }

      if (reduced()) {
        place(200, 138, 0, 1, 0);
        return;
      }

      var t = 0, last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          t += last ? Math.min(50, now - last) : 16;
          last = now;
          frame(t % cycle);
        }
      });
      frame(0);
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-no-results-search]').forEach(function (el) {
        if (el._nrs) return;   // idempotent
        el._nrs = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-no-results-search></div>
<h2>No results for &ldquo;unicorns&rdquo;</h2>