← Buttons

Shine Button

Hover the button and a glint sweeps across it, a broad slanted band with a thin streak chasing it, like light catching glass. Loop it and it shines by itself every few seconds to draw the eye.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-shine-button to a button or link with a solid or gradient background.

Options

Add any of these attributes to the same element.

data-loopAlso shine by itself every few seconds while in view.
data-intervalTime between those shines. Default 2600 ms.
data-durationHow long one sweep takes. Default 800 ms.
data-colorColour of the glint. Default white.

Keyboard focus sets it off too.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Shine Button: hover the button and a bright glint sweeps across it, a
    // broad slanted band with a thin streak chasing it, like light catching
    // glass. Keyboard focus sets it off too.
    // Usage: <button data-shine-button>Get the deal</button>
    // Optional: data-color (glint colour, default white), data-duration (ms
    // one sweep takes, default 800), data-loop (also shine by itself every few
    // seconds while in view, to draw the eye), data-interval (ms between those
    // shines, default 2600).
    var DEFAULTS = { duration: 800, interval: 2600 };

    var css =
      '.shb{position:relative;overflow:hidden;isolation:isolate}' +
      '.shb-glint{position:absolute;top:-20%;bottom:-20%;left:0;width:100%;pointer-events:none;z-index:1;transform:translateX(-100%)}' +
      '.shb-glint::before, .shb-glint::after{content:"";position:absolute;top:0;bottom:0;transform:skewX(-22deg)}' +
      '.shb-glint::before{left:20%;width:34%;' +
      'background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--shb-color) 55%,transparent),transparent)}' +
      '.shb-glint::after{left:62%;width:8%;' +
      'background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--shb-color) 70%,transparent),transparent)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(el) {
      var d = el.dataset;
      var duration = +d.duration || DEFAULTS.duration;
      el.classList.add('shb');
      var glint = document.createElement('span');
      glint.className = 'shb-glint';
      glint.setAttribute('aria-hidden', 'true');
      glint.style.setProperty('--shb-color', d.color || '#fff');
      el.appendChild(glint);
      var running = false;

      function shine() {
        if (running) return;
        running = true;
        // From just off the left edge to just off the right, quick in the middle.
        anime.animate(glint, {
          x: ['-100%', '100%'],
          duration: duration,
          ease: 'inOut(2.5)',
          onComplete: function () { running = false; }
        });
        // A small lift as the light passes.
        anime.animate(el, {
          filter: ['brightness(1)', 'brightness(1.12)', 'brightness(1)'],
          duration: duration,
          ease: 'inOut(2)'
        });
      }

      el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') shine(); });
      el.addEventListener('focus', shine);
      el.addEventListener('touchstart', shine, { passive: true });

      if (d.loop !== undefined && d.loop !== 'false') {
        var every = +d.interval || DEFAULTS.interval;
        var timer = null;
        var tick = function () {
          if (!el.matches(':hover')) shine();
          timer = setTimeout(tick, every);
        };
        new IntersectionObserver(function (entries) {
          clearTimeout(timer);
          if (entries[0].isIntersecting) timer = setTimeout(tick, 500);
        }).observe(el);
      }
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-shine-button]').forEach(function (el) {
        if (el._shb) return;
        el._shb = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<a class="button" href="/pricing" data-shine-button>Get the deal</a>

<!-- shining every few seconds: -->
<a class="button" href="/pricing" data-shine-button data-loop>Get the deal</a>