More Animations Snippets
Text Generate Effect — Free HTML CSS JS Reveal Snippet
Text Generate Effect · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Text Generate Effect — Word-by-Word Blur-In Reveal

The text-generate effect is the headline animation popularized by AI products: instead of appearing all at once, a sentence materializes word by word, each one un-blurring and rising into place on a gentle stagger, as if the page is "thinking" the text into existence. This snippet recreates it in plain HTML, CSS, and vanilla JavaScript, with scroll-triggered playback and gradient-highlighted accent words.
Splitting text into animatable words
The source sentence lives in a data-text attribute. On load, JavaScript splits it on spaces and wraps each word in a <span class="tg-word">, rejoining them with spaces so the natural line-wrapping is preserved. Wrapping each word individually is what lets them animate independently — you can't transition parts of a single text node, so the split into spans is the foundation of the whole effect.
The blur-in transition
Each word starts at opacity: 0, filter: blur(10px), and translateY(6px). Adding an .in class transitions all three to their resting values over half a second, so the word sharpens from a soft blur, fades up, and settles. The blur is the signature touch — it reads as "rendering" rather than a plain fade, which is exactly the generative-AI aesthetic. Each word uses display: inline-block so the transform applies (transforms are ignored on inline elements) and white-space: pre so spacing stays intact.
The stagger
The animation feels alive because words don't appear simultaneously. A loop schedules each word's .in class with setTimeout at 120 + i * 90 milliseconds, so word i starts 90ms after the one before it. That cascade is what makes the sentence appear to type or stream itself in. All timers are tracked in an array and cleared before each run so replays don't overlap or double-fire.
Scroll-triggered playback
A headline that animates before it's on screen is wasted, so an IntersectionObserver watches the element and only calls run() when at least 40% of it enters the viewport, then disconnect()s so it fires once. This is the standard, performant way to trigger scroll animations — no scroll listener, no per-frame math, and the browser handles the visibility detection.
Gradient accent words
Specific words listed in the ACCENT array get an extra class that, once revealed, paints them with a background-clip: text gradient so they stand out in color while the rest stay white. Because the gradient is applied only in the .in state, the accent words blur in like the others and then resolve into color, which keeps the emphasis from being visible before the animation reaches them.
Replayable
A replay button clears the timers, strips the .in classes, and re-runs the stagger, so you can demo the effect repeatedly. The same run() function powers both the initial scroll trigger and the replay.
Customizing it
Change the per-word delay to speed up or slow down the "generation," adjust the blur amount for a softer or sharper materialize, edit the ACCENT list to highlight different words, or swap the gradient colors. For a character-by-character variant, split on '' instead of ' '. Pair it with a sparkles text flourish or a shimmer button call to action beneath the headline.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA headline waits until it scrolls into view.
- 2Scroll it into viewWords un-blur and rise in one after another on a stagger.
- 3Watch the accentsHighlighted words resolve into a gradient as they appear.
- 4Click ReplayThe sentence clears and regenerates word by word.
- 5Edit the textChange the data-text attribute to your own sentence.
- 6Tune the timingAdjust the per-word delay and blur amount.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
You can't transition parts of a single text node, so each word is wrapped in its own inline-block span. That lets every word carry its own opacity, blur, and transform and animate independently, which is what makes the staggered word-by-word reveal possible. Rejoining with spaces preserves natural line wrapping.
Each word starts at filter: blur(10px) and transitions to blur(0). The blur reads as the text rendering or focusing into existence rather than a plain fade, which is the signature of generative-AI interfaces. Combined with a slight upward translate and fade, it feels like the words are being thought into place.
An IntersectionObserver watches the heading and runs the animation when 40% of it enters the viewport, then disconnects so it fires once. This avoids animating off-screen, needs no scroll listener or per-frame math, and lets the browser handle visibility detection efficiently.
Words listed in the ACCENT array get an extra class whose gradient (via background-clip: text) only applies in the revealed .in state. So accent words blur in like the rest and then resolve into color exactly when they appear, rather than showing their highlight before the animation reaches them.
Split the text into word spans in render rather than innerHTML, and drive each word's in class from state toggled on a stagger (a setTimeout loop or a CSS transition-delay based on index). Trigger with an IntersectionObserver in a mount effect, clearing timers on cleanup. In Tailwind, use blur and opacity utilities with arbitrary transition-delay-[...] values per word index.
Text Generate Effect — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Text Generate Effect</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a12;color:#f4f4f8;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:30px}
.tg-stage{max-width:680px;text-align:center}
.tg-eyebrow{font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#7c7c95;margin-bottom:18px}
.tg-text{font-size:clamp(26px,5.5vw,46px);font-weight:800;line-height:1.22;letter-spacing:-.02em}
.tg-word{display:inline-block;opacity:0;filter:blur(10px);transform:translateY(6px);transition:opacity .5s ease,filter .5s ease,transform .5s ease;white-space:pre}
.tg-word.in{opacity:1;filter:blur(0);transform:none}
.tg-word.accent.in{background:linear-gradient(120deg,#a78bfa,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent}
.tg-replay{margin-top:30px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);color:#cfcfe0;font-family:inherit;font-size:13px;font-weight:600;padding:9px 16px;border-radius:999px;cursor:pointer;transition:background .2s}
.tg-replay:hover{background:rgba(255,255,255,.12)}
</style>
</head>
<body>
<div class="tg-stage">
<p class="tg-eyebrow">AI-style reveal</p>
<h1 class="tg-text" id="tgText" data-text="Ideas become interfaces the moment you stop typing and start shipping."></h1>
<button type="button" class="tg-replay" id="tgReplay">↻ Replay</button>
</div>
<script>
var el = document.getElementById('tgText');
var replay = document.getElementById('tgReplay');
var ACCENT = ['interfaces', 'shipping.']; // words to gradient-highlight
function build() {
var words = el.getAttribute('data-text').split(' ');
el.innerHTML = words.map(function (w) {
var accent = ACCENT.indexOf(w) > -1 ? ' accent' : '';
return '<span class="tg-word' + accent + '">' + w + '</span>';
}).join(' ');
return Array.prototype.slice.call(el.querySelectorAll('.tg-word'));
}
var spans = build();
var timers = [];
function run() {
timers.forEach(clearTimeout); timers = [];
spans.forEach(function (s) { s.classList.remove('in'); });
// Stagger each word's blur-in by 90ms so the sentence "types" itself in.
spans.forEach(function (s, i) {
timers.push(setTimeout(function () { s.classList.add('in'); }, 120 + i * 90));
});
}
// Only start when the heading scrolls into view.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { run(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(el);
replay.addEventListener('click', run);
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Text Generate Effect</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0a0a12;color:#f4f4f8;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:30px
}
.tg-word {
display:inline-block;opacity:0;filter:blur(10px);transform:translateY(6px);transition:opacity .5s ease,filter .5s ease,transform .5s ease;white-space:pre
}
.tg-word.in {
opacity:1;filter:blur(0);transform:none
}
.tg-word.accent.in {
background:linear-gradient(120deg,#a78bfa,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent
}
</style>
</head>
<body>
<div class="max-w-[680px] text-center">
<p class="text-xs font-bold tracking-[.16em] uppercase text-[#7c7c95] mb-[18px]">AI-style reveal</p>
<h1 class="text-[clamp(26px,5.5vw,46px)] font-extrabold leading-[1.22] tracking-[-.02em]" id="tgText" data-text="Ideas become interfaces the moment you stop typing and start shipping."></h1>
<button type="button" class="mt-[30px] bg-[rgba(255,255,255,.06)] border border-[rgba(255,255,255,.14)] text-[#cfcfe0] font-[inherit] text-[13px] font-semibold py-[9px] px-4 rounded-[999px] cursor-pointer [transition:background_.2s] hover:bg-[rgba(255,255,255,.12)]" id="tgReplay">↻ Replay</button>
</div>
<script>
var el = document.getElementById('tgText');
var replay = document.getElementById('tgReplay');
var ACCENT = ['interfaces', 'shipping.']; // words to gradient-highlight
function build() {
var words = el.getAttribute('data-text').split(' ');
el.innerHTML = words.map(function (w) {
var accent = ACCENT.indexOf(w) > -1 ? ' accent' : '';
return '<span class="tg-word' + accent + '">' + w + '</span>';
}).join(' ');
return Array.prototype.slice.call(el.querySelectorAll('.tg-word'));
}
var spans = build();
var timers = [];
function run() {
timers.forEach(clearTimeout); timers = [];
spans.forEach(function (s) { s.classList.remove('in'); });
// Stagger each word's blur-in by 90ms so the sentence "types" itself in.
spans.forEach(function (s, i) {
timers.push(setTimeout(function () { s.classList.add('in'); }, 120 + i * 90));
});
}
// Only start when the heading scrolls into view.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { run(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(el);
replay.addEventListener('click', run);
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to TextGenerateEffect.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a12;color:#f4f4f8;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:30px}
.tg-stage{max-width:680px;text-align:center}
.tg-eyebrow{font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#7c7c95;margin-bottom:18px}
.tg-text{font-size:clamp(26px,5.5vw,46px);font-weight:800;line-height:1.22;letter-spacing:-.02em}
.tg-word{display:inline-block;opacity:0;filter:blur(10px);transform:translateY(6px);transition:opacity .5s ease,filter .5s ease,transform .5s ease;white-space:pre}
.tg-word.in{opacity:1;filter:blur(0);transform:none}
.tg-word.accent.in{background:linear-gradient(120deg,#a78bfa,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent}
.tg-replay{margin-top:30px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);color:#cfcfe0;font-family:inherit;font-size:13px;font-weight:600;padding:9px 16px;border-radius:999px;cursor:pointer;transition:background .2s}
.tg-replay:hover{background:rgba(255,255,255,.12)}
`;
export default function TextGenerateEffect() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var el = document.getElementById('tgText');
var replay = document.getElementById('tgReplay');
var ACCENT = ['interfaces', 'shipping.']; // words to gradient-highlight
function build() {
var words = el.getAttribute('data-text').split(' ');
el.innerHTML = words.map(function (w) {
var accent = ACCENT.indexOf(w) > -1 ? ' accent' : '';
return '<span class="tg-word' + accent + '">' + w + '</span>';
}).join(' ');
return Array.prototype.slice.call(el.querySelectorAll('.tg-word'));
}
var spans = build();
var timers = [];
function run() {
timers.forEach(clearTimeout); timers = [];
spans.forEach(function (s) { s.classList.remove('in'); });
// Stagger each word's blur-in by 90ms so the sentence "types" itself in.
spans.forEach(function (s, i) {
timers.push(setTimeout(function () { s.classList.add('in'); }, 120 + i * 90));
});
}
// Only start when the heading scrolls into view.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { run(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(el);
replay.addEventListener('click', run);
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="tg-stage">
<p className="tg-eyebrow">AI-style reveal</p>
<h1 className="tg-text" id="tgText" data-text="Ideas become interfaces the moment you stop typing and start shipping."></h1>
<button type="button" className="tg-replay" id="tgReplay">↻ Replay</button>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function TextGenerateEffect() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var el = document.getElementById('tgText');
var replay = document.getElementById('tgReplay');
var ACCENT = ['interfaces', 'shipping.']; // words to gradient-highlight
function build() {
var words = el.getAttribute('data-text').split(' ');
el.innerHTML = words.map(function (w) {
var accent = ACCENT.indexOf(w) > -1 ? ' accent' : '';
return '<span class="tg-word' + accent + '">' + w + '</span>';
}).join(' ');
return Array.prototype.slice.call(el.querySelectorAll('.tg-word'));
}
var spans = build();
var timers = [];
function run() {
timers.forEach(clearTimeout); timers = [];
spans.forEach(function (s) { s.classList.remove('in'); });
// Stagger each word's blur-in by 90ms so the sentence "types" itself in.
spans.forEach(function (s, i) {
timers.push(setTimeout(function () { s.classList.add('in'); }, 120 + i * 90));
});
}
// Only start when the heading scrolls into view.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { run(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(el);
replay.addEventListener('click', run);
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0a0a12;color:#f4f4f8;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:30px
}
.tg-word {
display:inline-block;opacity:0;filter:blur(10px);transform:translateY(6px);transition:opacity .5s ease,filter .5s ease,transform .5s ease;white-space:pre
}
.tg-word.in {
opacity:1;filter:blur(0);transform:none
}
.tg-word.accent.in {
background:linear-gradient(120deg,#a78bfa,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent
}
`}</style>
<div className="max-w-[680px] text-center">
<p className="text-xs font-bold tracking-[.16em] uppercase text-[#7c7c95] mb-[18px]">AI-style reveal</p>
<h1 className="text-[clamp(26px,5.5vw,46px)] font-extrabold leading-[1.22] tracking-[-.02em]" id="tgText" data-text="Ideas become interfaces the moment you stop typing and start shipping."></h1>
<button type="button" className="mt-[30px] bg-[rgba(255,255,255,.06)] border border-[rgba(255,255,255,.14)] text-[#cfcfe0] font-[inherit] text-[13px] font-semibold py-[9px] px-4 rounded-[999px] cursor-pointer [transition:background_.2s] hover:bg-[rgba(255,255,255,.12)]" id="tgReplay">↻ Replay</button>
</div>
</>
);
}<template>
<div class="tg-stage">
<p class="tg-eyebrow">AI-style reveal</p>
<h1 class="tg-text" id="tgText" data-text="Ideas become interfaces the moment you stop typing and start shipping."></h1>
<button type="button" class="tg-replay" id="tgReplay">↻ Replay</button>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let el;
let replay;
var ACCENT = ['interfaces', 'shipping.'];
// words to gradient-highlight
function build() {
var words = el.getAttribute('data-text').split(' ');
el.innerHTML = words.map(function (w) {
var accent = ACCENT.indexOf(w) > -1 ? ' accent' : '';
return '<span class="tg-word' + accent + '">' + w + '</span>';
}).join(' ');
return Array.prototype.slice.call(el.querySelectorAll('.tg-word'));
}
let spans;
var timers = [];
function run() {
timers.forEach(clearTimeout); timers = [];
spans.forEach(function (s) { s.classList.remove('in'); });
// Stagger each word's blur-in by 90ms so the sentence "types" itself in.
spans.forEach(function (s, i) {
timers.push(setTimeout(function () { s.classList.add('in'); }, 120 + i * 90));
});
}
let io;
onMounted(() => {
el = document.getElementById('tgText');
replay = document.getElementById('tgReplay');
spans = build();
io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { run(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(el);
replay.addEventListener('click', run);
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a12;color:#f4f4f8;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:30px}
.tg-stage{max-width:680px;text-align:center}
.tg-eyebrow{font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#7c7c95;margin-bottom:18px}
.tg-text{font-size:clamp(26px,5.5vw,46px);font-weight:800;line-height:1.22;letter-spacing:-.02em}
.tg-word{display:inline-block;opacity:0;filter:blur(10px);transform:translateY(6px);transition:opacity .5s ease,filter .5s ease,transform .5s ease;white-space:pre}
.tg-word.in{opacity:1;filter:blur(0);transform:none}
.tg-word.accent.in{background:linear-gradient(120deg,#a78bfa,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent}
.tg-replay{margin-top:30px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);color:#cfcfe0;font-family:inherit;font-size:13px;font-weight:600;padding:9px 16px;border-radius:999px;cursor:pointer;transition:background .2s}
.tg-replay:hover{background:rgba(255,255,255,.12)}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-text-generate-effect',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="tg-stage">
<p class="tg-eyebrow">AI-style reveal</p>
<h1 class="tg-text" id="tgText" data-text="Ideas become interfaces the moment you stop typing and start shipping."></h1>
<button type="button" class="tg-replay" id="tgReplay">↻ Replay</button>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a12;color:#f4f4f8;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:30px}
.tg-stage{max-width:680px;text-align:center}
.tg-eyebrow{font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#7c7c95;margin-bottom:18px}
.tg-text{font-size:clamp(26px,5.5vw,46px);font-weight:800;line-height:1.22;letter-spacing:-.02em}
.tg-word{display:inline-block;opacity:0;filter:blur(10px);transform:translateY(6px);transition:opacity .5s ease,filter .5s ease,transform .5s ease;white-space:pre}
.tg-word.in{opacity:1;filter:blur(0);transform:none}
.tg-word.accent.in{background:linear-gradient(120deg,#a78bfa,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent}
.tg-replay{margin-top:30px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);color:#cfcfe0;font-family:inherit;font-size:13px;font-weight:600;padding:9px 16px;border-radius:999px;cursor:pointer;transition:background .2s}
.tg-replay:hover{background:rgba(255,255,255,.12)}
`]
})
export class TextGenerateEffectComponent implements AfterViewInit {
ngAfterViewInit(): void {
var el = document.getElementById('tgText');
var replay = document.getElementById('tgReplay');
var ACCENT = ['interfaces', 'shipping.']; // words to gradient-highlight
function build() {
var words = el.getAttribute('data-text').split(' ');
el.innerHTML = words.map(function (w) {
var accent = ACCENT.indexOf(w) > -1 ? ' accent' : '';
return '<span class="tg-word' + accent + '">' + w + '</span>';
}).join(' ');
return Array.prototype.slice.call(el.querySelectorAll('.tg-word'));
}
var spans = build();
var timers = [];
function run() {
timers.forEach(clearTimeout); timers = [];
spans.forEach(function (s) { s.classList.remove('in'); });
// Stagger each word's blur-in by 90ms so the sentence "types" itself in.
spans.forEach(function (s, i) {
timers.push(setTimeout(function () { s.classList.add('in'); }, 120 + i * 90));
});
}
// Only start when the heading scrolls into view.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) { run(); io.disconnect(); } });
}, { threshold: 0.4 });
io.observe(el);
replay.addEventListener('click', run);
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { build, run });
}
}