← E-commerce

Featured Plan Highlight

In a row of pricing plans, the recommended one lifts up and starts to glow while the others step back a touch. A “Most popular” badge drops onto its top edge with a bounce, and a soft shine sweeps across it.

Basic £5/mo
  • 1 site
  • 5 GB
  • Email help
Pro £12/mo
  • 10 sites
  • 50 GB
  • Live chat
Team £29/mo
  • Unlimited
  • 1 TB
  • Phone line

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-featured-plan-highlight to the row of plan cards.
  3. Mark the recommended card with data-featured. Without it, the middle card is picked.

Options

Add any of these attributes to the same element.

data-badgeBadge text. Default “Most popular”.
data-colorGlow and badge colour. Default #6a0dad.
data-liftHow far the card rises. Default 12 px.
data-loopSettle and highlight again every few seconds while in view.

It plays once when scrolled into view. Keep room above the row for the badge, which sits half over the card’s top edge.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Featured Plan Highlight: in a row of pricing plans, the recommended one
    // lifts up and glows while the others step back a touch, and a "Most
    // popular" badge drops onto its top edge with a bounce. A soft shine then
    // sweeps across it.
    // Usage: <div data-featured-plan-highlight>
    //          <div>Basic…</div><div data-featured>Pro…</div><div>Team…</div>
    //        </div>
    // Optional: data-badge (badge text, default "Most popular"), data-color
    // (glow and badge colour, default #6a0dad), data-lift (how far it rises in
    // px, default 12), data-loop (settle and highlight again every few seconds
    // while in view). Mark the plan with data-featured; default the middle one.
    var DEFAULTS = { badge: 'Most popular', color: '#6a0dad', lift: 12, hold: 2800, gap: 1000 };

    var css =
      '.fph-card{position:relative;transform-origin:50% 100%}' +
      '.fph-glow{position:absolute;inset:-2px;border-radius:inherit;pointer-events:none;opacity:0;' +
      'box-shadow:0 0 0 2px var(--fph-c),0 .9em 2.2em -.3em var(--fph-c)}' +
      '.fph-shine{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none}' +
      '.fph-shine::before{content:"";position:absolute;top:0;bottom:0;left:-60%;width:45%;' +
      'background:linear-gradient(100deg,transparent,rgba(255,255,255,.65),transparent);transform:translateX(var(--fph-x,0))}' +
      '.fph-badge{position:absolute;left:50%;top:0;z-index:2;white-space:nowrap;padding:.3em .8em;border-radius:999px;background:var(--fph-c);color:#fff;' +
      'font-size:.72em;font-weight:700;letter-spacing:.02em;line-height:1.2;box-shadow:0 .3em .8em rgba(15,15,30,.2);opacity:0;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

    function setup(el) {
      var d = el.dataset;
      var cards = Array.prototype.slice.call(el.children);
      if (!cards.length) return;
      var pick = el.querySelector(':scope > [data-featured]') || cards[Math.floor((cards.length - 1) / 2)];
      el.style.setProperty('--fph-c', d.color || DEFAULTS.color);
      cards.forEach(function (c) { c.classList.add('fph-card'); });
      var glow = document.createElement('span');
      glow.className = 'fph-glow';
      var shine = document.createElement('span');
      shine.className = 'fph-shine';
      var badge = document.createElement('span');
      badge.className = 'fph-badge';
      badge.textContent = d.badge || DEFAULTS.badge;
      [glow, shine].forEach(function (x) { x.setAttribute('aria-hidden', 'true'); pick.appendChild(x); });
      pick.appendChild(badge);
      var s = el._fph = {
        pick: pick, others: cards.filter(function (c) { return c !== pick; }), glow: glow, shine: shine, badge: badge,
        lift: d.lift !== undefined ? +d.lift : DEFAULTS.lift, on: false, busy: false, visible: false
      };
      // Centre the badge on the card's top edge without fighting anime's transforms.
      var place = function () { badge.style.marginLeft = -badge.offsetWidth / 2 + 'px'; badge.style.marginTop = -badge.offsetHeight / 2 + 'px'; };
      place();
      new ResizeObserver(place).observe(badge);
      if (reduced()) { highlight(el); return; }
      new IntersectionObserver(function (entries) {
        s.visible = entries[0].isIntersecting;
        if (s.visible && !s.busy && (!s.played || s.waiting)) { s.waiting = false; setTimeout(function () { highlight(el); }, 250); }
      }, { threshold: 0.3 }).observe(el);
    }

    function highlight(el) {
      var s = el._fph, d = el.dataset;
      if (s.busy || s.on) return;
      s.busy = true;
      s.played = true;
      s.pick.style.zIndex = 1;
      var done = function () {
        s.busy = false;
        s.on = true;
        if (on(d.loop)) setTimeout(function () { settle(el); }, DEFAULTS.hold);
      };
      if (reduced()) {
        anime.utils.set(s.pick, { y: -s.lift, scale: 1.05 });
        anime.utils.set([s.glow, s.badge], { opacity: 1 });
        anime.utils.set(s.others, { opacity: 0.75 });
        done();
        return;
      }
      var tl = anime.createTimeline({ onComplete: done });
      tl.add(s.others, { scale: [1, 0.96], opacity: [1, 0.72], duration: 500, ease: 'out(3)' }, 0)
        .add(s.pick, { y: [0, -s.lift], scale: [1, 1.05], duration: 650, ease: 'outBack(1.4)' }, 0)
        .add(s.glow, { opacity: [0, 1], duration: 600, ease: 'out(2)' }, 100)
        .add(s.badge, { y: ['-2.2em', '0em'], opacity: [0, 1], rotate: [-10, 0], duration: 800, ease: 'outBounce' }, 350)
        .add(s.shine, { '--fph-x': ['0%', '360%'], duration: 900, ease: 'inOut(2)' }, 900);
    }

    function settle(el) {
      var s = el._fph;
      if (!s.visible) { s.waiting = true; s.on = false; resetNow(s); return; }
      s.busy = true;
      var tl = anime.createTimeline({
        onComplete: function () {
          s.busy = false;
          s.on = false;
          s.pick.style.zIndex = '';
          setTimeout(function () { if (s.visible) highlight(el); else s.waiting = true; }, DEFAULTS.gap);
        }
      });
      tl.add(s.badge, { y: '-1.2em', opacity: 0, duration: 300, ease: 'in(2)' }, 0)
        .add(s.glow, { opacity: 0, duration: 400 }, 100)
        .add(s.pick, { y: 0, scale: 1, duration: 500, ease: 'inOut(2)' }, 150)
        .add(s.others, { scale: 1, opacity: 1, duration: 500, ease: 'inOut(2)' }, 150);
    }

    function resetNow(s) {
      anime.utils.set(s.badge, { opacity: 0 });
      anime.utils.set(s.glow, { opacity: 0 });
      anime.utils.set(s.pick, { y: 0, scale: 1 });
      anime.utils.set(s.others, { scale: 1, opacity: 1 });
    }

    function init() {
      document.querySelectorAll('[data-featured-plan-highlight]').forEach(function (el) {
        if (el._fph) return;
        el._fph = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="plans" data-featured-plan-highlight>
  <div class="plan">Basic …</div>
  <div class="plan" data-featured>Pro …</div>
  <div class="plan">Team …</div>
</div>