TextFree

Letter Fade In

Each letter fades in with a staggered delay: splitText() wraps every character in a <span> with a progressive animation-delay — a single CSS keyframe animates opacity 0 → 1.

JSCSSStagger
F A D E I N
Hover or click the scene to interact

This effect is free — the Text category contains 56 effects total, including 4 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

Effect.Labs Library

① Landing hero — Tagline revealed letter by letter

WhenOn landing on a product page or portfolio, when the main headline needs to grab attention at first glance.
WhyThe progressive reveal creates a micro-moment of anticipation that reinforces reading of the headline and conveys careful staging — no canvas, no library needed.
SettingsdelayStep 0.06 s, font-size 4 rem, font-weight 900, letter-spacing 0.05 em, white text on dark background.
👋
New login detected
Your workspace is ready.

② Onboarding card — First name revealed on first login

WhenOn a user's first login, inside a welcome modal or card that displays their first name.
WhyRevealing a name letter by letter turns a static text into a memorable moment without weighing down the component — the effect is brief (under 0.5 s for 5 letters) and blocks no interaction.
SettingsdelayStep 0.05 s for a brisk reveal, animation-duration 0.4 s, font-size 2 rem, the rest of the component stays static.
The Night of Origins

③ Narrative title — Chapter opening or game cinematic

WhenIn a game, presentation, or interactive story, when a chapter title or key phrase must appear dramatically.
WhyA longer delayStep (0.12 s) slows the reveal and builds narrative tension — each letter becomes a beat, like a film title card or graphic novel chapter heading.
SettingsdelayStep 0.12 s, font-size 3 rem, font-weight 700, letter-spacing 0.2 em, amber on a very dark background.

How it works

The function splitText(element, text, delayStep) splits the string character by character. Spaces are preserved as plain text; each letter is wrapped in a <span style="animation-delay: Ns">, with the delay equal to the character index multiplied by delayStep (0.05 s in the function signature, 0.08 s in initFadeIn()). The result is injected as the innerHTML of the target element.

CSS drives all the animation: .text-fade-in span starts at opacity: 0 and runs @keyframes fadeInLetter (opacity: 1 at 100%) over 0.5 s, easing ease, fill-mode: forwards — each letter stays visible after its animation ends. The staggered delays mechanically produce the cascade reveal.

The replayFadeIn() function clears the innerHTML, waits 50 ms (to let the browser commit removal of the frozen animation styles), then calls initFadeIn() again. That minimal delay is necessary: a forwards keyframe stays locked on its final state as long as the node exists — clearing and re-injecting the DOM is the most reliable way to restart from scratch.

The effect is 100% CSS for the animation: no requestAnimationFrame, no canvas, no external library. JS only builds the initial DOM and handles replay. The splitText() function is generic — it accepts any element and any string, and can be called multiple times to animate independent text zones.

Accessibility

  • prefers-reduced-motion not present in the source code: no system preference detection. Add manually for auto-started uses: @media (prefers-reduced-motion: reduce) { .text-fade-in span { animation: none; opacity: 1; } } — letters then appear immediately with no transition.
  • Characters remain real text nodes in the DOM — screen readers read the full word normally, without spelling it letter by letter (the <span> elements carry no ARIA role, which is correct for decorative content).
  • The Replay button is a native <button>, keyboard-focusable. No :focus-visible style is defined in the source — add one to match your project's design system.
  • Contrast: the effect imposes no text color (inherits from parent). The contrast ratio depends entirely on the background and color chosen by the integrator.
  • The animated container carries no aria-label or special role — since the content is real readable text, this is correct in most cases; in a purely decorative context, add aria-hidden="true" on the container.

Browser compatibility

Pure CSS animation (opacity + animation-delay) and vanilla JS — supported in all modern browsers. Zero external dependencies.

Chrome 80+✓ Full
Firefox 75+✓ Full
Safari 13.1+✓ Full
Edge 80+✓ Full
Mobile iOS 13+✓ Full
Android Chrome✓ Full

If JS is disabled, the static HTML structure preserves the <code>&lt;span&gt;</code> elements and CSS animation still works. If CSS animations are disabled browser-side, letters appear directly at <code>opacity: 1</code> — no errors, the text remains readable.

The code

Copy the three blocks into your page. No dependencies.

HTML
<span class="demo-text text-fade-in" id="fadeInText">
  <span style="animation-delay: 0s">F</span>
  <span style="animation-delay: 0.08s">A</span>
  <span style="animation-delay: 0.16s">D</span>
  <span style="animation-delay: 0.24s">E</span> <span style="animation-delay: 0.4s">I</span>
  <span style="animation-delay: 0.48s">N</span>
</span>
<button class="replay-btn" onclick="replayFadeIn()">Replay</button>
CSS
.demo-text  {
   font-size: 2.5rem;
   font-weight: 800;
   letter-spacing: -0.02em;
   }

.text-fade-in span  {
   opacity: 0;
   animation: 0.5s ease 0s 1 normal forwards running fadeInLetter;
   }

@keyframes fadeInLetter  {
   
  100%  {
   opacity: 1;
   }
}

.replay-btn  {
   position: absolute;
   bottom: 15px;
   right: 15px;
   padding: 8px 16px;
   background: rgba(99, 102, 241, 0.2);
   border: 1px solid rgba(99, 102, 241, 0.4);
   border-radius: var(--radius-md);
   color: var(--primary-light);
   font-size: 0.8rem;
   font-weight: 500;
   cursor: pointer;
   transition: all var(--transition-fast);
   }

.replay-btn:hover  {
   background: rgba(99, 102, 241, 0.4);
   transform: scale(1.05);
   }
JavaScript (fx-0653)
function splitText(element, text, delayStep = 0.05) {
      element.innerHTML = text.split('').map((char, i) => {
        if (char === ' ') return ' ';
        return `<span style="animation-delay: ${i * delayStep}s">${char}</span>`;
      }).join('');
    }

// 1. Letter Fade In
    function initFadeIn() {
      const el = document.getElementById('fadeInText');
      splitText(el, 'FADE IN', 0.08);
    }

function replayFadeIn() {
      const el = document.getElementById('fadeInText');
      el.innerHTML = '';
      setTimeout(() => initFadeIn(), 50);
    }

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
delayStep (3rd arg of splitText) 0.08 s Delay gap between consecutive letters. 0.04 s = brisk reveal, 0.15 s = dramatic and slow.
0.5s ease (CSS animation property) 0.5 s, ease Duration and easing for each letter's fade. Lower to 0.3 s for a punchy effect, raise to 0.8 s for a soft fade. Accepts any CSS easing (cubic-bezier, ease-out…).
@keyframes fadeInLetter (CSS) opacity 0→1 Only opacity is animated. Add from { opacity:0; transform:translateY(10px) } to { opacity:1; transform:translateY(0) } to slide each letter upward while fading in.
font-size: 2.5rem (.demo-text) 2.5 rem Size of the animated text. Adapt to layout: 1.5 rem for a label, 5 rem for a full-screen hero headline.
font-weight: 800 (.demo-text) 800 Text weight. 400 for a subtle body-text effect, 900 for maximum impact.
letter-spacing: -0.02em (.demo-text) -0.02 em Letter spacing. Set to 0.2 em for a wide editorial look, -0.04 em for an ultra-compact headline.
splitText(el, 'YOUR TEXT', 0.08) The function is fully generic: call it on any DOM element with any string. Call it multiple times to animate several independent text zones.

FAQ

Can the effect animate multiple independent text zones on the same page?

Yes. splitText() is generic: call it on as many elements as needed with distinct identifiers. Only initFadeIn() is tied to #fadeInText — for other zones, pass the DOM element directly as the first argument: splitText(document.getElementById('my-title'), 'MY TEXT', 0.08).

Why is the 50 ms delay in replayFadeIn() necessary?

A CSS animation with fill-mode: forwards stays locked on its final state as long as the node exists in the DOM. Clearing the innerHTML removes the spans; the 50 ms lets the browser commit the removal before new spans (starting at opacity: 0) are inserted. Without this delay, the browser may recycle nodes and fail to restart the animation from scratch.

How do I add a vertical slide (slide up) alongside the fade?

Modify the fadeInLetter keyframe to start from a vertical offset: from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); }. Make sure the parent container has no overflow: hidden that would clip the lower letters, or adjust its padding-bottom accordingly.