UI Components Easy
Text Rotate
Animated text that cycles through a list of words with smooth enter/exit transitions — fade, slide, or typewriter style.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 640px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2.5rem;
}
.section {
margin-bottom: 2.5rem;
}
.section-label {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #475569;
margin-bottom: 0.875rem;
}
/* ── Headline ── */
.headline {
font-size: clamp(1.5rem, 4vw, 2.25rem);
font-weight: 700;
line-height: 1.2;
color: #f2f6ff;
}
/* ── Rotating word wrapper ── */
.text-rotate {
display: inline-block;
position: relative;
color: #60a5fa;
white-space: nowrap;
min-width: 2ch;
}
/* ── Fade mode ── */
.text-rotate[data-mode="fade"] .tr-word {
display: inline-block;
}
.text-rotate[data-mode="fade"] .tr-word.tr-enter {
animation: tr-fade-in 0.4s ease forwards;
}
.text-rotate[data-mode="fade"] .tr-word.tr-exit {
animation: tr-fade-out 0.35s ease forwards;
position: absolute;
left: 0;
top: 0;
}
@keyframes tr-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes tr-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* ── Slide mode ── */
.text-rotate[data-mode="slide"] {
overflow: hidden;
vertical-align: bottom;
line-height: 1.25;
/* height is set by JS to match line height */
}
.text-rotate[data-mode="slide"] .tr-word {
display: inline-block;
}
.text-rotate[data-mode="slide"] .tr-word.tr-enter {
animation: tr-slide-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.text-rotate[data-mode="slide"] .tr-word.tr-exit {
animation: tr-slide-out 0.3s cubic-bezier(0.55, 0, 1, 0.45) forwards;
position: absolute;
left: 0;
top: 0;
}
@keyframes tr-slide-in {
from {
transform: translateY(110%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes tr-slide-out {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-110%);
opacity: 0;
}
}
/* ── Typewriter mode ── */
.text-rotate[data-mode="type"] .tr-cursor {
display: inline-block;
width: 2px;
height: 1em;
background: #60a5fa;
margin-left: 2px;
vertical-align: middle;
animation: tr-blink 0.9s step-end infinite;
}
@keyframes tr-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}(function () {
"use strict";
/**
* TextRotate — cycles through words using fade, slide, or typewriter animation.
* @param {HTMLElement} el — .text-rotate element
*/
function TextRotate(el) {
const words = JSON.parse(el.dataset.words || "[]");
const mode = el.dataset.mode || "fade";
const interval = parseInt(el.dataset.interval || "2500", 10);
if (words.length < 2) return;
let index = 0;
let timer = null;
let typing = false;
if (mode === "type") {
initTypewriter();
} else {
initSwap();
}
/* ── Fade / Slide ──────────────────────────────── */
function initSwap() {
// Render first word
el.innerHTML = `<span class="tr-word">${words[0]}</span>`;
if (mode === "slide") setSlideHeight();
timer = setInterval(nextSwap, interval);
}
function setSlideHeight() {
const span = el.querySelector(".tr-word");
if (span) el.style.height = span.offsetHeight + "px";
}
function nextSwap() {
const next = (index + 1) % words.length;
const current = el.querySelector(".tr-word:not(.tr-exit)");
if (!current) return;
// Exit the current word
current.classList.add("tr-exit");
current.addEventListener("animationend", () => current.remove(), { once: true });
// Enter the next word
const entering = document.createElement("span");
entering.className = "tr-word tr-enter";
entering.textContent = words[next];
el.appendChild(entering);
entering.addEventListener("animationend", () => entering.classList.remove("tr-enter"), {
once: true,
});
index = next;
}
/* ── Typewriter ────────────────────────────────── */
function initTypewriter() {
const textNode = document.createElement("span");
textNode.className = "tr-text";
const cursor = document.createElement("span");
cursor.className = "tr-cursor";
cursor.setAttribute("aria-hidden", "true");
el.innerHTML = "";
el.appendChild(textNode);
el.appendChild(cursor);
typeWord(words[0], textNode, () => {
setTimeout(scheduleNext, interval - words[0].length * 60 - 600);
});
}
function typeWord(word, node, onDone) {
node.textContent = "";
typing = true;
let i = 0;
const speed = Math.max(40, Math.min(90, Math.floor(interval / (word.length * 3))));
const t = setInterval(() => {
node.textContent = word.slice(0, ++i);
if (i >= word.length) {
clearInterval(t);
typing = false;
onDone && onDone();
}
}, speed);
}
function eraseWord(node, onDone) {
typing = true;
const erase = setInterval(() => {
const cur = node.textContent;
if (cur.length === 0) {
clearInterval(erase);
typing = false;
onDone && onDone();
return;
}
node.textContent = cur.slice(0, -1);
}, 40);
}
function scheduleNext() {
const textNode = el.querySelector(".tr-text");
if (!textNode) return;
eraseWord(textNode, () => {
index = (index + 1) % words.length;
const word = words[index];
setTimeout(() => {
typeWord(textNode, word, () => {
setTimeout(scheduleNext, interval - word.length * 60 - 600);
});
}, 120);
});
}
}
// Initialise all instances on the page
document.querySelectorAll(".text-rotate").forEach((el) => new TextRotate(el));
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Text Rotate</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Text Rotate</h1>
<p class="demo-sub">Animated word cycling with fade, slide, and typewriter modes.</p>
<section class="section">
<p class="section-label">Fade</p>
<p class="headline">
Built for
<span
class="text-rotate"
data-words='["designers","developers","founders","creators","makers"]'
data-mode="fade"
data-interval="2200"
aria-live="polite"
>designers</span>
</p>
</section>
<section class="section">
<p class="section-label">Slide up</p>
<p class="headline">
Ship faster with
<span
class="text-rotate"
data-words='["Astro","React","Tailwind","TypeScript","Bun"]'
data-mode="slide"
data-interval="2000"
aria-live="polite"
>Astro</span>
</p>
</section>
<section class="section">
<p class="section-label">Typewriter</p>
<p class="headline">
We love
<span
class="text-rotate"
data-words='["open source","the web","clean code","good docs","dark mode"]'
data-mode="type"
data-interval="2800"
aria-live="polite"
>open source</span>
</p>
</section>
</div>
<script src="script.js"></script>
</body>
</html>Text Rotate
Cycles through an array of words with CSS-driven transitions. Three animation styles included.
Variants
- Fade — opacity cross-fade between words
- Slide up — new word slides in from below, old one exits upward
- Typewriter — characters appear one by one with a blinking cursor
Usage
Set the word list in data-words as a JSON array on the .text-rotate element. Choose the animation style with a data-mode attribute: fade, slide, or type. The JS reads those attributes and drives the animation loop automatically.