Next stage town
Stage 4 · 118 kmStart tomorrow 1 pm
A word split into letters that rise 20 px one after another, 80 ms apart, then replaced by the next word every 2.5 s: the only cascade that works on the text itself — 2 KB of JavaScript, your words written in the HTML, read as one whole word by screen readers, letters placed at once when the user reduces motion.
Updated
This effect is free — the Transitions category contains 52 effects total, including 9 free. Effect.Labs has 840 vanilla effects. Explore the category →
Next stage town
Stage 4 · 118 kmStart tomorrow 1 pm
Next class
10:15 am – 12:15 pm · first-year students
Doors open 10 min before240 places
BOOKSHOP · THE HIGH TIDE
Theme of the month
Our booksellers' picks on the front table
The .md-char-stage frame (250 px tall, background #1a1a24) centers .md-char-cascade. The script writes one letter per span.md-char into it: 2rem, weight 700, white, display: inline-block so the letter can move on its own. Each letter starts invisible and 20 px lower, and the mdCharIn animation lifts it in 0.4 s; forwards keeps it in place. The offset is written by the script: animation-delay is i × 0.08 s. A six-letter word is fully written in 0.8 s.
The script walks through every .md-char-cascade on the page. It reads its words from the container's children — one element per word, for example <span>VANNES</span><span>LORIENT</span> — and, if the container is empty, takes the demo list (MOTION, DESIGN, EFFECT, SMOOTH). showWord() empties the container, writes the whole word into a span.md-char-sr invisible to the eye, then the letters, each marked aria-hidden="true"; a space becomes a non-breaking space to keep its width. Every 2.5 s, the next word takes its place. An IntersectionObserver only runs this cycle while the block is visible; with a single word, there is no cycle, the cascade plays once.
The automatic cycle is the nature of the effect: a single word to read, changing at a steady pace, like a sign. Separate letters do not get in the way of assistive reading: measured in Chromium 151 and Firefox 153, the accessibility tree contains "MOTION" in one piece, not six letters. The frame has its own class, prefixed like the rest of the effect: two effects from the library pasted on the same page do not overwrite each other.
prefers-reduced-motion: reduce, the CSS puts each letter in place, with no rise (animation: none; opacity: 1; transform: none). The word keeps changing every 2.5 s, without motion. Measured in Chromium 151, Firefox 153 and WebKit 26: six letters at opacity 1 at the first measurement.span, and the animated letters are hidden from assistive technology. Measured: the accessibility tree gives "MOTION", then "DESIGN" 2.5 s later. The container has no aria-live: a change announced aloud every 2.5 s would be exhausting. If the word carries important information, also write it elsewhere on the page.clearInterval), or limit the number of rounds; on a public display screen (room door, shop window), the cycle is expected.#1a1a24, over 15:1 once in place; during the 0.4 s rise, opacity grows from 0 to 1.CSS (@keyframes, transform) + 2 KB of JavaScript (IntersectionObserver, setInterval), no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same cascade, same word change at 2.5 s, same whole word exposed to screen readers, same immediate placement under reduced motion.
Without IntersectionObserver, the cycle runs permanently. Without JavaScript, your words written in the container show as they are, side by side and without animation: keep only the first word in the HTML if the no-script display matters, and add the others from the script.
Copy the three blocks into your page. No dependencies.
<div class="md-char-stage">
<div class="md-char-cascade"></div>
</div>
.md-char-stage,
.md-char-stage *,
.md-char-stage *::before,
.md-char-stage *::after {
box-sizing: border-box
}
.md-char-stage {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.md-char-cascade {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.md-char {
display: inline-block;
font-size: 2rem;
font-weight: 700;
color: #fff;
opacity: 0;
transform: translateY(20px);
animation: mdCharIn 0.4s forwards;
}
@keyframes mdCharIn {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Le mot entier, lu par les lecteurs d'écran, invisible à l'œil */
.md-char-sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
/* Mouvement réduit : les lettres sont posées d'emblée, le mot change sans mouvement */
@media (prefers-reduced-motion: reduce) {
.md-char {
animation: none;
opacity: 1;
transform: none;
}
}
(function() {
// Character Cascade : le mot monte lettre par lettre, puis cède la place au suivant toutes les 2,5 s
document.querySelectorAll('.md-char-cascade').forEach(function(container) {
// Les mots : un élément enfant par mot (<span>VANNES</span><span>LORIENT</span>),
// sinon la liste de démonstration
var words = Array.prototype.map.call(container.children, function(el) {
return el.textContent.trim();
}).filter(Boolean);
if (!words.length) words = ['MOTION', 'DESIGN', 'EFFECT', 'SMOOTH'];
var currentWord = 0;
var intervalId = null;
function showWord() {
// Animations mises en pause (bouton Pause de la page, outils d'accessibilité) : on garde le mot affiché
var first = container.querySelector('.md-char');
if (first && getComputedStyle(first).animationPlayState === 'paused') return;
container.innerHTML = '';
var word = words[currentWord];
// Le mot entier pour les lecteurs d'écran ; les lettres, masquées pour eux, sont pour l'œil
var label = document.createElement('span');
label.className = 'md-char-sr';
label.textContent = word;
container.appendChild(label);
word.split('').forEach(function(char, i) {
var span = document.createElement('span');
span.className = 'md-char';
span.setAttribute('aria-hidden', 'true');
span.textContent = char === ' ' ? '\u00a0' : char;
span.style.animationDelay = (i * 0.08) + 's';
container.appendChild(span);
});
currentWord = (currentWord + 1) % words.length;
}
showWord();
if (words.length < 2) return;
// Le cycle ne tourne que lorsque le bloc est visible
if ('IntersectionObserver' in window) {
var obs = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting) {
if (!intervalId) intervalId = setInterval(showWord, 2500);
} else {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
}, {
threshold: 0
});
obs.observe(container);
} else {
// Repli : cycle permanent
intervalId = setInterval(showWord, 2500);
}
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Your words (HTML — children of .md-char-cascade) |
none: MOTION, DESIGN, EFFECT, SMOOTH | One element per word, in display order: <span>LAW</span><span>CHEMISTRY</span>. A single word: the cascade plays once, with no cycle. |
Gap between letters (JS — i * 0.08) |
80 ms | 40 ms for a long word (12 letters in 0.84 s); 120 ms for a typewriter feel. Keep the full writing under half the cycle. |
Cycle pace (JS — setInterval(showWord, 2500)) |
2.5 s per word | 4 s for a display read from afar; below 1.5 s, the word has no time to be read after its cascade. |
Rise and duration (CSS — translateY(20px), mdCharIn 0.4s) |
20 px, 0.4 s | translateY(0.6em) keeps the same proportion at any text size; rotateX(90deg) instead gives letters that flip up. |
Typography (CSS — .md-char) |
2rem, weight 700, white | Any font works, each letter being an inline-block box. Ligatures and kerning are lost: prefer capitals or an evenly spaced font for large headings. |
The word. The script writes the whole word into a span.md-char-sr, hidden from the eye but read by assistive technology, and marks each animated letter aria-hidden="true". Measured in Chromium and Firefox: the accessibility tree contains "MOTION" in one piece. Keep the .md-char-sr CSS rule if you change the styling: without it, the word would show twice on screen.
In showWord(), after currentWord = (currentWord + 1) % words.length;, clear the interval when currentWord is back to 0: if (currentWord === 0) { clearInterval(intervalId); obs.disconnect(); }. The last word stays displayed. For a pause button, simply call clearInterval(intervalId).
Spaces are kept (they become non-breaking spaces), so "BELLE ÎLE" is written correctly. But a line of inline-block letters does not wrap between words: beyond 15 to 20 characters, reduce the size or split the phrase into several containers, one per line.