Gradient Border Spin
A band of colour travels round and round the button’s border, with a soft glow of the same colours spilling out behind it. Hover it and it surges faster and glows brighter.
How to use it
- Paste the snippet just before
</head>. - Add
data-gradient-border-spinto a button or link with a solid background.
Options
Add any of these attributes to the same element.
data-colors | Comma-separated colours for the band. Default #a855f7,#ec4899,#f59e0b. |
data-width | Border width in pixels. Default 2. |
data-speed | Time for one lap. Default 3000 ms. |
data-glow="false" | Turn off the glow. |
It only spins while it’s on screen. It looks best on a dark button on a dark background.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Gradient Border Spin: a band of colour travels round and round the
// button's border, with a soft glow of the same colours spilling out behind
// it. Hover or focus the button and it speeds up and glows brighter. It
// spins while it's on screen.
// Usage: <button data-gradient-border-spin>Start free trial</button>
// Optional: data-colors (comma-separated, default "#a855f7,#ec4899,#f59e0b"),
// data-width (border width in px, default 2), data-speed (ms per lap,
// default 3000), data-glow="false" (no glow).
// The button keeps its own background; give it a solid one.
var DEFAULTS = { colors: '#a855f7,#ec4899,#f59e0b', width: 2, speed: 3000 };
// The layers are cut out with masks so they never cover the button: the
// ring keeps only the border band, the glow only what's outside the button.
var cut = 'linear-gradient(#000 0 0) content-box exclude,linear-gradient(#000 0 0)';
var cutWebkit = 'linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0)';
var css =
'.gbs{position:relative;isolation:isolate}' +
'.gbs-ring,.gbs-glow i{position:absolute;inset:calc(var(--gbs-w) * -1);padding:var(--gbs-w);pointer-events:none;' +
'background:conic-gradient(from var(--gbs-a,0deg),var(--gbs-colors));' +
'-webkit-mask:' + cutWebkit + ';-webkit-mask-composite:xor;mask:' + cut + '}' +
// The glow: a blurred copy of the ring (blurred as a whole, in the middle
// layer), with everything over the button masked away again.
'.gbs-glow{position:absolute;inset:-20px;padding:20px;pointer-events:none;z-index:-1;opacity:var(--gbs-glow,.6);' +
'-webkit-mask:' + cutWebkit + ';-webkit-mask-composite:xor;mask:' + cut + '}' +
'.gbs-glow b{position:absolute;inset:20px;filter:blur(9px)}' +
'.gbs-glow i{padding:calc(var(--gbs-w) * 2.5)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(el) {
var d = el.dataset;
var colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
// The gradient ends where it started, so there's no seam.
colors.push(colors[0]);
el.classList.add('gbs');
el.style.setProperty('--gbs-colors', colors.join(','));
el.style.setProperty('--gbs-w', (+d.width || DEFAULTS.width) + 'px');
// The glow sits outside the button, so match its rounding to the ring's.
var radius = parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0;
var ringRadius = radius ? radius + (+d.width || DEFAULTS.width) + 'px' : '0';
var ring = document.createElement('span');
ring.className = 'gbs-ring';
ring.setAttribute('aria-hidden', 'true');
ring.style.borderRadius = ringRadius;
el.appendChild(ring);
var glow = null;
if (d.glow !== 'false') {
glow = document.createElement('span');
glow.className = 'gbs-glow';
glow.setAttribute('aria-hidden', 'true');
glow.style.borderRadius = radius ? radius + 20 + 'px' : '0';
glow.innerHTML = '<b><i style="border-radius:' + ringRadius + '"></i></b>';
el.appendChild(glow);
}
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var spin = anime.animate(el, {
'--gbs-a': ['0deg', '360deg'],
duration: +d.speed || DEFAULTS.speed,
ease: 'linear',
loop: true,
autoplay: false
});
var hot = false;
function heat(on) {
if (hot === on) return;
hot = on;
// Ease the playback speed rather than jumping, so the band surges.
anime.animate(spin, { speed: on ? 3 : 1, duration: on ? 400 : 900, ease: 'out(2)' });
if (glow) anime.animate(el, { '--gbs-glow': on ? 1 : 0.6, duration: 400, ease: 'out(2)' });
}
el.addEventListener('pointerenter', function () { heat(true); });
el.addEventListener('pointerleave', function () { heat(document.activeElement === el); });
el.addEventListener('focus', function () { heat(true); });
el.addEventListener('blur', function () { heat(el.matches(':hover')); });
// Only spin while visible, to save battery.
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) spin.play();
else spin.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-gradient-border-spin]').forEach(function (el) {
if (el._gbs) return;
el._gbs = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a class="button" href="/signup" data-gradient-border-spin>Start free trial</a>
<!-- thicker, in your colours: -->
<a class="button" href="/beta" data-gradient-border-spin data-colors="#22d3ee,#a3e635" data-width="3">Join the beta</a>