Blind tasting · Thursday evening
Wine no. 4 was…
Every 3 s, the word on screen scrambles into randomly drawn characters, then settles left to right, one letter fixed every 100 ms, into the next word: a "decryption"-style decode in an indigo-pink gradient. CSS and 1.9 KB of JavaScript, no AI model, word list in the HTML, a direct switch when the user reduces motion.
Updated
This effect is free — the Liquid category contains 31 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
Blind tasting · Thursday evening
Wine no. 4 was…
Wi-Fi network TIERS-LIEU · today's code:
The code changes every morning at 8 am · ask at the bar if needed
| Time | Callsign | Band | Signal |
|---|---|---|---|
| 14:27 | OZ1QRV | 20 m | −18 dB |
| 14:19 | I2WXQ | 20 m | −09 dB |
| 14:06 | LA9ZQK | 40 m | −21 dB |
The word is a <div class="ai-typo-word"> centered in .ai-typo-morph: 2.5rem, weight 800, 2 px of letter spacing, painted with an indigo, violet, pink gradient (#6366f1 → #a855f7 → #ec4899) clipped to the letter shapes (background-clip: text). The word list sits in its data-words attribute, comma-separated; the first word is written in the HTML, so it shows without the script.
Every 3 s, the script reads data-words again, takes the next word and starts the decode: every 50 ms, it rewrites the whole text. The letter at position i is fixed as soon as the pass counter exceeds 2 × i: one more letter becomes right every 100 ms, left to right; the others are drawn at random from 66 characters (A to Z, a to z, 0 to 9, @ # $ %), and spaces stay spaces. After length × 2 + 5 passes, the exact word is set: 0.85 s for a 6-letter word, 1.35 s for "CHINON 2019". The only randomness is that character draw (Math.random): the catalog name "AI" covers no model and no artificial intelligence.
The script handles every .ai-typo-word on the page, each with its own list and timer, and does nothing while the word is off screen (IntersectionObserver) or the tab is hidden. Since the list is read again at each change, the page can modify it: a one-word list stops the cycle on that word. The drawn characters do not have the same width as the final letters: in a proportional font, the word breathes a little during the decode; a monospace font keeps the line still, useful for a code or a callsign. Measured in Chromium 151, Firefox 153 and WebKit 26: DESIGN, CREATE, MORPH, DREAM, in order, each decode finished before the next one.
prefers-reduced-motion: reduce, the word switches at once every 3 s, with no scrambled character. Measured in all three engines: over 7 s, only complete words appear.aria-live region, so nothing is announced; but a screen reader reading it at that moment hears gibberish. If the word carries information (a code, a name to reveal), put aria-hidden="true" on the animated element and write the final word in visually hidden text, updated once the decode is done.CSS (background-clip: text) + 1.9 KB of JavaScript (setInterval, IntersectionObserver, matchMedia), no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same word order, same pace of one letter every 100 ms, same direct switch under reduced motion; off-screen pause checked in Chromium and Firefox.
Without JavaScript, the first word written in the HTML stays on screen, in its gradient. Without IntersectionObserver, the cycle keeps running off screen. Without background-clip: text (a rare case), the word shows on a gradient block: then remove -webkit-text-fill-color and give the text a color.
Copy the three blocks into your page. No dependencies.
<div class="fx-0419-root">
<div class="ai-typo-morph">
<div class="ai-typo-word" data-words="DESIGN, CREATE, MORPH, DREAM, BUILD">DESIGN</div>
</div>
</div>
.fx-0419-root,
.fx-0419-root *,
.fx-0419-root *::before,
.fx-0419-root *::after {
box-sizing: border-box
}
.fx-0419-root {
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;
}
.ai-typo-morph {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.ai-typo-word {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #6366f1, #a855f7, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 2px;
}
(function() {
// AI Typo Morph: the word scrambles, then settles letter by letter
// Words: data-words attribute, comma-separated (read again at each change)
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%';
document.querySelectorAll('.ai-typo-word').forEach(function(wordEl) {
function readWords() {
return (wordEl.getAttribute('data-words') || '').split(',')
.map(function(w) { return w.trim(); })
.filter(Boolean);
}
var currentWord = 0;
var scramble = 0;
var onScreen = true;
if (!wordEl.textContent.trim()) wordEl.textContent = readWords()[0] || '';
function morphTo(target) {
clearInterval(scramble);
// Reduced motion: the word switches at once, no scrambling
if (reduce.matches) {
wordEl.textContent = target;
return;
}
var iterations = 0;
scramble = setInterval(function() {
var result = '';
for (var i = 0; i < target.length; i++) {
if (iterations > i * 2 || target[i] === ' ') {
result += target[i];
} else {
result += chars[Math.floor(Math.random() * chars.length)];
}
}
wordEl.textContent = result;
iterations++;
if (iterations > target.length * 2 + 5) {
clearInterval(scramble);
wordEl.textContent = target;
}
}, 50);
}
// Pause while the block is off screen
if ('IntersectionObserver' in window) {
new IntersectionObserver(function(entries) {
onScreen = entries[0].isIntersecting;
}).observe(wordEl);
}
setInterval(function() {
var words = readWords();
if (!onScreen || document.hidden || words.length < 2) return;
currentWord = (currentWord + 1) % words.length;
morphTo(words[currentWord]);
}, 3000);
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Words (HTML — data-words on .ai-typo-word) |
DESIGN, CREATE, MORPH, DREAM, BUILD |
Words, codes or phrases separated by commas; write the first one in the element. Read again at each change: a one-word list freezes the display on that word. |
Pace (JS — 3000 in setInterval) |
3 s between two words | Keep it above the decode time (length × 100 ms + 250 ms): 2 s is enough for 6-letter words, 5 s leaves time to read a code. |
Decode speed (JS — 50 ms and i * 2) |
one letter fixed every 100 ms | i * 4 fixes a letter every 200 ms, for a longer suspense; i * 1 every 50 ms, for a lightning decode. |
Drawn characters (JS — chars) |
A–Z, a–z, 0–9, @ # $ % | '0123456789' for a counter or a numeric code, '01' for a binary look, symbols ('░▒▓█') for a more graphic scramble. |
Font and gradient (CSS — .ai-typo-word) |
site font, 2.5rem, weight 800, indigo-violet-pink gradient | font-family: monospace keeps the word from changing width during the scramble; the gradient can be replaced by any color or gradient. |
No. The name comes from the catalog, but the script only draws characters at random (Math.random) and then fixes the target word's letters one by one, left to right. No model, no network call: the final word is always the one written in data-words.
Write two words in the list, the placeholder text and then the word to reveal (data-words="WINE NO. 4, CHINON 2019"). After the first decode, replace the list with the revealed word alone (el.dataset.words = 'CHINON 2019'): with a single word, the script changes nothing more. To trigger the reveal on click instead of after 3 s, start with a single word in the list and add the second one on click.
Text Morph goes from one word to the next with a blurred fade: you only ever see whole words, readable at every moment. AI Typo Morph shows the transition: the word scrambles into random characters, then rebuilds letter by letter. Choose AI Typo Morph for a code, a name to reveal or a "signal" mood; Text Morph for words that must stay readable (languages, jobs, products).