Rotating Text
A heading with one rotating word slot that cycles through a comma-separated list. Each word slides out upward and the next slides in from below, with the wrapper width animating smoothly between word lengths.
GSAPSplitTextTypographyLoopAnimation
Setup — External Scripts
GSAP CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>SplitText CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/SplitText.min.js"></script>Code
index.html
html
<h1 data-rotating-title class="rotating-text__heading">Simple
<span data-rotating-words="routines, tools, systems, help" class="rotating-text__highlight">routines</span>
that give growing and ambitious teams more clarity.
</h1>styles.css
css
.rotating-text__heading {
text-align: center;
letter-spacing: -.02em;
margin: 0;
font-family: Haffer, Arial, sans-serif;
font-size: clamp(2.5em, 7.5vw, 4.5em);
font-weight: 500;
line-height: 1;
}
.rotating-text__highlight {
color: #33de96;
}
[data-rotating-words] {
display: inline-block;
position: relative;
}
.rotating-text__inner {
display: inline-block;
}
.rotating-text__word {
display: block;
white-space: nowrap;
position: absolute;
top: 0;
left: 0;
}
.rotating-line {
padding-bottom: 0.1em;
margin-bottom: -0.1em;
white-space: nowrap;
}
.rotating-line-mask {
overflow-x: visible !important;
overflow-y: clip !important;
}script.js
javascript
function initRotatingText() {
document.querySelectorAll('[data-rotating-title]').forEach(heading => {
const stepDuration = parseFloat(heading.getAttribute('data-step-duration') || '1.75');
SplitText.create(heading, {
type: 'lines',
mask: 'lines',
autoSplit: true,
linesClass: 'rotating-line',
onSplit(instance) {
const rotatingSpan = heading.querySelector('[data-rotating-words]');
if (!rotatingSpan) return;
const rawWords = rotatingSpan.getAttribute('data-rotating-words') || '';
const words = rawWords.split(',').map(w => w.trim()).filter(Boolean);
if (!words.length) return;
const wrapper = document.createElement('span');
wrapper.className = 'rotating-text__inner';
const wordEls = words.map(word => {
const el = document.createElement('span');
el.className = 'rotating-text__word';
el.textContent = word;
wrapper.appendChild(el);
return el;
});
rotatingSpan.textContent = '';
rotatingSpan.appendChild(wrapper);
requestAnimationFrame(() => {
const inDuration = 0.75;
const outDuration = 0.6;
gsap.set(wordEls, { yPercent: 150, autoAlpha: 0 });
let activeIndex = 0;
const firstWord = wordEls[activeIndex];
gsap.set(firstWord, { yPercent: 0, autoAlpha: 1 });
wrapper.style.width = firstWord.getBoundingClientRect().width + 'px';
function showNext() {
const nextIndex = (activeIndex + 1) % wordEls.length;
const prev = wordEls[activeIndex];
const current = wordEls[nextIndex];
gsap.to(wrapper, { width: current.getBoundingClientRect().width, duration: inDuration, ease: 'power4.inOut' });
if (prev && prev !== current) {
gsap.to(prev, { yPercent: -150, autoAlpha: 0, duration: outDuration, ease: 'power4.inOut' });
}
gsap.fromTo(current,
{ yPercent: 150, autoAlpha: 0 },
{ yPercent: 0, autoAlpha: 1, duration: inDuration, ease: 'power4.inOut' }
);
activeIndex = nextIndex;
gsap.delayedCall(stepDuration, showNext);
}
if (wordEls.length > 1) gsap.delayedCall(stepDuration, showNext);
});
}
});
});
}
document.addEventListener("DOMContentLoaded", function () {
initRotatingText();
});Notes
- •All words from `data-rotating-words` are built as absolutely-positioned spans stacked inside a wrapper — only their `yPercent` and `autoAlpha` change during animation, never the DOM order.
- •The wrapper width animates to match each incoming word's measured pixel width, keeping surrounding text reflow-free.
- •The initial visible word in the HTML should be the longest word in the list so SplitText line-breaking is accurate before the dynamic words replace it.
- •Using `gsap.delayedCall` for the loop avoids `setInterval` drift and integrates cleanly with GSAP's global timeline control.
Guide
Rotating Words
Add `data-rotating-words` to a `<span>` inside the heading. The value is a comma-separated list of words to cycle through. Place the longest word as the visible DOM text so SplitText measures the correct line width before the stack is injected.
<h1 data-rotating-title>
Hello
<span data-rotating-words="word 1, word 2, word 3">word 1</span>
World
</h1>Step Duration
Use `data-step-duration` on the `[data-rotating-title]` element to set the pause between rotations in seconds (default 1.75).
<h1 data-rotating-title data-step-duration="2.5">
Hello <span data-rotating-words="word 1, word 2, word 3">word 1</span> World
</h1>