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
- Paste the snippet just before
</head>. - Add
data-featured-plan-highlightto the row of plan cards. - 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-badge | Badge text. Default “Most popular”. |
data-color | Glow and badge colour. Default #6a0dad. |
data-lift | How far the card rises. Default 12 px. |
data-loop | Settle 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.
<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><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>