Live preview needs JavaScript. The code below works without any dependencies.
Prompt for your AI assistant
Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.
Build "Letter pop intro" for my site using the Enrichr Library element at https://enrichr.dev/elements/letter-pop-intro.html.
Read the machine-readable spec and source at https://enrichr.dev/elements/letter-pop-intro.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.
Settings:
- text: Hello
- delay: 60
- overshoot: 1.25
- color: #ffffff
Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.The same spec as JSON, for tools that read it directly: /elements/letter-pop-intro.json.
Code
Mount it on an element that contains the markup. It returns an object with update(options) and destroy().
HTML
<h3 class="ef-pop"></h3>CSS
.ef-pop { margin: 0; font-size: clamp(2.4rem, 9vw, 6rem); font-weight: 800; line-height: 1; letter-spacing: -0.03em; }
.ef-pop__c { display: inline-block; opacity: 0; transform: scale(0.2) rotate(-12deg); }
.ef-pop.is-in .ef-pop__c { animation: ef-pop 700ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes ef-pop {
0% { opacity: 0; transform: scale(0.2) rotate(-12deg); }
60% { opacity: 1; transform: scale(var(--ef-o, 1.25)) rotate(3deg); }
100% { opacity: 1; transform: scale(1) rotate(0); }
}
@media (prefers-reduced-motion: reduce) { .ef-pop__c { opacity: 1; transform: none; animation: none !important; } }JavaScript
// Letter pop intro: each letter overshoots its size then settles, staggered left to right.
// mount(root, { text, delay (ms between letters), overshoot (1.0 - 1.6), color }) -> { update, replay, destroy }
function mount(root, o) {
var h = root.querySelector(".ef-pop");
var opts = {}, visible = false, io = null;
function render() {
h.textContent = "";
h.classList.remove("is-in");
h.setAttribute("aria-label", opts.text);
h.style.color = opts.color;
h.style.setProperty("--ef-o", String(opts.overshoot));
var n = 0;
Array.from(opts.text).forEach(function (ch) {
var s = document.createElement("span");
s.className = "ef-pop__c";
s.setAttribute("aria-hidden", "true");
s.textContent = ch === " " ? " " : ch;
s.style.animationDelay = n * opts.delay + "ms";
if (ch !== " ") n++;
h.appendChild(s);
});
if (visible) replay();
}
function replay() {
h.classList.remove("is-in");
void h.offsetWidth;
h.classList.add("is-in");
}
function update(next) {
opts = Object.assign({ text: "Hello", delay: 60, overshoot: 1.25, color: "#ffffff" }, opts, next || {});
render();
}
update(o);
if ("IntersectionObserver" in window) {
io = new IntersectionObserver(function (e) { if (e[0].isIntersecting && !visible) { visible = true; replay(); } }, { threshold: 0.2 });
io.observe(root);
} else { visible = true; replay(); }
return { update: update, replay: replay, destroy: function () { if (io) io.disconnect(); } };
}
// Usage
var fx = mount(document.querySelector("#my-root"), {"text":"Hello","delay":60,"overshoot":1.25,"color":"#ffffff"});How it works
Letters become inline-block spans, each with an animation-delay that grows by one step. A single keyframe scales from small and rotated, past the overshoot value, then to one. The overshoot is a CSS custom property, so changing it never rebuilds the DOM.
Accessibility and performance
The full text is the aria-label and the letters are hidden from screen readers. Reduced motion removes the animation and shows the text at once.
Transform and opacity only. Keep it to short text; a long sentence makes hundreds of animated spans.
| Section | Text |
|---|---|
| Techniques | Text animation, Stagger |
| Stack | Vanilla JS, CSS |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
Find this technique on a real site
Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.
Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns
Licence and provenance
Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.
This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.
More elements
FAQ
How long can the text be?
Keep it to a word or short phrase. Every letter is an element, so long paragraphs are better served by the split text reveal.
Can I trigger it on page load instead of scroll?
Call replay() when your loader finishes. It is built to be driven from outside.
Will it work with a variable font?
Yes. It only sets size and transform, so any font works, and a heavy weight suits the overshoot.