Rotating Circular Text
A line of text set round a circle turns slowly, like a sticker or a “scroll down” seal. Whatever you put inside sits still in the middle, and the ring winds up to speed while the pointer is over it.
↓
How to use it
- Paste the snippet just before
</head>. - Add
data-rotating-circular-text="Your text •"to an element. Anything inside it, such as an icon or an arrow, sits in the middle.
Options
Add any of these attributes to the same element.
data-size | The badge’s width. Default 160 px. |
data-speed | Time for one full turn. Default 12000 ms. |
data-direction | cw (default) for clockwise, or ccw. |
data-hover-speed | How much faster it turns while hovered. Default 4; 1 turns it off. |
The text is spaced out to go exactly once round, so about 20 to 40 characters looks best. Repeat a short phrase to fill the ring.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Rotating Circular Text: a line of text set round a circle turns slowly,
// like a badge or a "scroll down" seal. Whatever is inside the element sits
// still in the middle, and the ring speeds up while the pointer is over it.
// Usage: <div data-rotating-circular-text="Made with care • Made with care •">★</div>
// Optional: data-size (width of the badge in px, default 160), data-speed
// (ms for one full turn, default 12000), data-direction ("cw" (default) or
// "ccw"), data-hover-speed (how much faster it turns on hover, default 4;
// 1 turns that off).
var DEFAULTS = { size: 160, speed: 12000, hoverSpeed: 4 };
var NS = 'http://www.w3.org/2000/svg';
var uid = 0;
var css =
'.rct{position:relative;display:inline-grid;place-items:center;aspect-ratio:1;vertical-align:middle}' +
'.rct-ring{position:absolute;inset:0;width:100%;height:100%;overflow:visible}' +
'.rct-ring text{fill:currentColor;font-size:10.5px;font-weight:700;text-transform:uppercase}' +
'.rct-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function node(tag, attrs, parent) {
var n = document.createElementNS(NS, tag);
for (var k in attrs) n.setAttribute(k, attrs[k]);
parent.appendChild(n);
return n;
}
function build(el) {
var d = el.dataset;
var text = (el.getAttribute('data-rotating-circular-text') || 'Rotating text •').replace(/\s+/g, ' ').trim();
el.classList.add('rct');
if (!el.style.width) el.style.width = (+d.size || DEFAULTS.size) + 'px';
var sr = document.createElement('span');
sr.className = 'rct-sr';
sr.textContent = text;
el.insertBefore(sr, el.firstChild);
// A circle drawn as a path from the top, clockwise. The text is stretched
// to fit exactly once round, so the ring always closes up neatly.
var r = 40;
var id = 'rct-circle-' + ++uid;
var svg = node('svg', { class: 'rct-ring', viewBox: '0 0 100 100', 'aria-hidden': 'true' }, el);
node('path', { id: id, fill: 'none', d: 'M50 ' + (50 - r) + ' a' + r + ' ' + r + ' 0 1 1 0 ' + 2 * r + ' a' + r + ' ' + r + ' 0 1 1 0 ' + -2 * r }, svg);
var label = node('text', { dy: '0.35em', textLength: (2 * Math.PI * r - 1).toFixed(1), lengthAdjust: 'spacing' }, svg);
node('textPath', { href: '#' + id }, label).textContent = text + ' ';
return svg;
}
function start(el, ring) {
var d = el.dataset;
var dir = d.direction === 'ccw' ? -1 : 1;
var anim = anime.animate(ring, {
rotate: [0, dir * 360],
duration: +d.speed || DEFAULTS.speed,
ease: 'linear',
loop: true
});
// On hover the ring winds up to speed, and eases back down when you leave.
var fast = +d.hoverSpeed || DEFAULTS.hoverSpeed;
if (fast !== 1) {
var to = function (v) { anime.animate(anim, { speed: v, duration: 600, ease: 'out(3)' }); };
el.addEventListener('pointerenter', function () { to(fast); });
el.addEventListener('pointerleave', function () { to(1); });
}
return anim;
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-rotating-circular-text]').forEach(function (el) {
if (el._rct) return;
var ring = (el._rct = build(el));
// Reduced motion: the badge stays still.
if (reduced) return;
var anim = null;
// Turn only while visible, to save battery.
new IntersectionObserver(function (entries) {
if (!entries[0].isIntersecting) { if (anim) anim.pause(); return; }
if (anim) anim.play();
else anim = start(el, ring);
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a href="#work" data-rotating-circular-text="Scroll to explore • Scroll to explore •">↓</a>