Follow Toggle
Press “+ Follow” and the plus spins away as a tick draws itself in, the label slides up to “Following” and the button eases to its new width, turning outlined with a little pop and a puff of dots. Press again to unfollow.
How to use it
- Paste the snippet just before
</head>. - Add
data-follow-toggleto a button. The script adds the plus and tick in front of your label. - Listen for its
changeevent;event.detail.followingsays which way it went.
Options
Add any of these attributes to the same element.
data-on | Label while following. Default “Following”. |
data-color | Colour of the button. Default #6a0dad. |
data-following | Start out following. |
data-autoplay | It toggles itself every couple of seconds while in view, until someone presses it. Handy for demos. |
While following the button has the class flw-on and aria-pressed="true". Size it with font-size. With reduced motion the label just swaps.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Follow Toggle: a "+ Follow" button. Press it and the plus spins away as
// a tick draws itself in, the label slides to "Following", the button
// eases to its new width and swaps to an outlined style with a little
// pop and a puff of dots. Press again to unfollow.
// Usage: <button data-follow-toggle>Follow</button>
// Optional: data-on (label while following, default "Following"),
// data-color (colour of the button, default #6a0dad), data-following
// (start out following), data-autoplay (it toggles itself every couple of
// seconds while in view, until someone presses it; handy for demos).
// Events: fires "change" on the button with event.detail.following.
// While following it has the class flw-on and aria-pressed="true".
var DEFAULTS = { on: 'Following', color: '#6a0dad', every: 2400 };
var ICONS = '<svg class="flw-icon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path class="flw-plus" d="M12 5v14M5 12h14"/><path class="flw-tick" pathLength="1" d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
var css =
':where(.flw){font:inherit;font-weight:700;padding:.7em 1.3em;border-radius:999px}' +
'.flw{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.4em;cursor:pointer;white-space:nowrap;' +
'background:var(--flw-c);color:#fff;border:2px solid var(--flw-c);' +
'box-shadow:0 8px 20px color-mix(in srgb,var(--flw-c) 28%,transparent);transition:background-color .3s,color .3s,box-shadow .3s}' +
'.flw.flw-on{background:#fff;color:var(--flw-c);box-shadow:0 3px 10px rgba(15,15,30,.08)}' +
'.flw-icon{width:1em;height:1em;flex:none;overflow:visible}' +
'.flw-plus,.flw-tick{transform-box:fill-box;transform-origin:center}' +
'.flw-tick{stroke-dasharray:1 1;stroke-dashoffset:1}' +
'.flw-labels{display:inline-grid;justify-items:start;overflow:hidden;padding:.1em 0}' +
'.flw-labels>span{grid-area:1/1}' +
'.flw-dot{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--flw-c);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 setup(el) {
var d = el.dataset;
el.classList.add('flw');
el.style.setProperty('--flw-c', d.color || DEFAULTS.color);
var offText = el.textContent.replace(/^\s*\+\s*/, '').trim() || 'Follow';
el.textContent = '';
el.insertAdjacentHTML('afterbegin', ICONS);
var plus = el.querySelector('.flw-plus');
var tick = el.querySelector('.flw-tick');
var labels = document.createElement('span');
labels.className = 'flw-labels';
var off = document.createElement('span');
off.textContent = offText;
var on = document.createElement('span');
on.textContent = d.on || DEFAULTS.on;
labels.appendChild(off);
labels.appendChild(on);
el.appendChild(labels);
var following = false, anim = null;
function puff() {
var n = 8, r = Math.max(el.offsetWidth, el.offsetHeight) * 0.62;
for (var i = 0; i < n; i++) {
var dot = document.createElement('span');
dot.className = 'flw-dot';
dot.setAttribute('aria-hidden', 'true');
el.appendChild(dot);
var a = (i / n) * Math.PI * 2 + 0.3;
anime.animate(dot, {
x: [0, Math.cos(a) * r], y: [0, Math.sin(a) * r * 0.55], scale: [1, 0.2], opacity: [0.9, 0],
duration: 620, ease: 'out(3)', onComplete: dot.remove.bind(dot)
});
}
}
function show(state, animate) {
following = state;
el.classList.toggle('flw-on', state);
el.setAttribute('aria-pressed', state ? 'true' : 'false');
var t = animate && !reduced() ? 1 : 0;
var a = state ? on : off, b = state ? off : on;
a.removeAttribute('aria-hidden');
b.setAttribute('aria-hidden', 'true');
// Ease the label box between the two labels' widths, then let the
// shown label size it again (so late-loading fonts still fit).
a.style.display = b.style.display = '';
var from = b.offsetWidth, to = a.offsetWidth;
if (anim) anim.pause();
anim = anime.animate(labels, {
width: [from, to], duration: 380 * t, ease: 'inOut(3)',
onComplete: function () { labels.style.width = ''; b.style.display = 'none'; }
});
anime.animate(b, { y: t ? (state ? '-110%' : '110%') : 0, opacity: 0, duration: 240 * t, ease: 'in(2)' });
anime.animate(a, { y: [t ? (state ? '110%' : '-110%') : 0, 0], opacity: [0, 1], duration: 340 * t, delay: 90 * t, ease: 'out(3)' });
anime.animate(plus, { rotate: state ? 135 : 0, scale: state ? 0 : 1, opacity: state ? 0 : 1, duration: 300 * t, ease: state ? 'in(2)' : 'outBack(2)' });
anime.animate(tick, { strokeDashoffset: state ? 0 : 1, duration: (state ? 380 : 180) * t, delay: state ? 160 * t : 0, ease: 'out(2)' });
if (!t) return;
if (state) {
anime.animate(el, { scale: [1, 0.9, 1.08, 1], duration: 560, ease: 'out(2)' });
puff();
} else {
anime.animate(el, { scale: [1, 0.95, 1], duration: 300, ease: 'out(2)' });
}
}
show(d.following !== undefined && d.following !== 'false', false);
var userTouched = false;
el.addEventListener('click', function (e) {
if (e.isTrusted) userTouched = true;
show(!following, true);
el.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { following: following } }));
});
if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null;
var loop = function () {
if (userTouched) return;
if (!el.matches(':hover')) show(!following, true);
timer = setTimeout(loop, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(loop, 800);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-follow-toggle]').forEach(function (el) {
if (el._flw) return;
el._flw = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-follow-toggle>Follow</button>
<!-- in pink, already following: -->
<button data-follow-toggle data-color="#db2777" data-following>Follow</button>