Copied!
Text

Letter pop intro

Each letter overshoots its size and settles, staggered left to right, for a bold page intro.

← Library

Element · Vanilla JS + CSSUpdated November 5, 2026

TextElementVanilla JS + CSS

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.

SectionText
TechniquesText animation, Stagger
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested

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.

Split text revealA headline whose words rise out of a mask one after another when it scrolls into view. Marquee tickerEndless horizontal text that pauses on hover, with speed, gap and direction controls. Word read highlightWords in a paragraph light up one by one as it scrolls through the viewport.

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.