Vanilla Examples

Common animation patterns using the splitText function with vanilla JavaScript.

Basic Fade In

Split text into words and animate each one with a staggered fade and slide effect.

Fade in each word

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');
const { words } = splitText(element);

animate(
  words,
  { opacity: [0, 1], y: [20, 0] },
  { delay: stagger(0.05), duration: 0.5 }
);

Character Reveal

Animate each character individually for a more granular effect. Great for headings and short text.

Character by character

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');
const { chars } = splitText(element);

animate(
  chars,
  { opacity: [0, 1], scale: [0.5, 1] },
  { delay: stagger(0.02), duration: 0.3 }
);

Line by Line

Animate each line separately. Lines are detected based on actual rendered positions, so this works with any text that wraps naturally.

This paragraph animates line by line. Each line slides in from the left.

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');
const { lines } = splitText(element);

animate(
  lines,
  { opacity: [0, 1], x: [-20, 0] },
  { delay: stagger(0.1), duration: 0.6 }
);

Nested Elements

Fetta preserves inline HTML elements like <a>, <em>, and <strong> when splitting text. All attributes (href, class, id, data-*, etc.) are maintained on the split output.

Click this link or see emphasized and bold text

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');
// HTML: Click <a href="#">this link</a> or see <em>emphasized</em> text

const { chars } = splitText(element);

animate(
  chars,
  { opacity: [0, 1], y: [10, 0] },
  { delay: stagger(0.02), duration: 0.3 }
);

Masked Line Reveal

Use the mask option to wrap elements in a clipping container with overflow: clip. This creates clean reveal animations where content slides into view from outside its bounds.

Each line reveals from below with a clipping mask for a clean effect.

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');

splitText(element, {
  type: 'lines',
  mask: 'lines',
  onSplit: ({ lines }) => {
    animate(
      lines,
      { y: ['100%', '0%'] },
      { delay: stagger(0.1), duration: 0.5 }
    );
  }
});

With Auto-Revert

Use onSplit with revertOnComplete: true to automatically restore the original HTML when the animation finishes. Return your animation from the callback - Fetta handles extracting the .finished promise.

Animating

Auto-revert after animation

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');

splitText(element, {
  onSplit: ({ chars }) =>
    animate(chars, { opacity: [0, 1], y: [10, 0] }, { delay: stagger(0.02), duration: 0.3 }),
  revertOnComplete: true
});

Responsive Split

Enable autoSplit to automatically re-split text when the container resizes. Use onResize to re-trigger animations when the text reflows to different lines.

If you only want to animate once on load, skip onResize and trigger your animation externally. Fetta will still re-split the text on resize to maintain correct line detection, but won't call any callback.

This text reflows naturally at any width, animating again when lines change.

import { splitText } from 'fetta';
import { animate, stagger } from 'motion';

const element = document.querySelector('p');

const result = splitText(element, {
  autoSplit: true,
  onResize: ({ lines }) => {
    animate(
      lines,
      { opacity: [0, 1], y: [12, 0] },
      { delay: stagger(0.08), duration: 0.4 }
    );
  }
});

// Cleanup when done (optional - auto-cleans when element removed from DOM)
result.revert();

Scroll-Triggered

Use Motion's inView to detect when the element enters the viewport and trigger the animation. Return a cleanup function to reset styles when the element leaves view.

Scroll down

Reveals on scroll

import { splitText } from 'fetta';
import { animate, stagger, inView } from 'motion';

const element = document.querySelector('p');
const { words } = splitText(element);

// Hide words initially
words.forEach(w => {
  w.style.opacity = '0';
  w.style.transform = 'translateY(30px)';
});

// Trigger animation when element enters viewport
inView(element, () => {
  animate(words, { opacity: [0, 1], y: [30, 0] }, { delay: stagger(0.03) });
  // Reset styles when leaving view
  return () => {
    words.forEach(w => {
      w.style.opacity = '0';
      w.style.transform = 'translateY(30px)';
    });
  };
}, { amount: 0.5 });

Scroll-Driven

Link animations to scroll progress using Motion's scroll() function. The animation plays as the target element moves through the viewport.

Scroll to animate

Each word reveals as you scroll through this container

import { splitText } from 'fetta';
import { animate, scroll } from 'motion';

const target = document.querySelector('.text-container');
const element = target.querySelector('p');
const { words } = splitText(element);

const animation = animate(
  words.map((word, i) => [
    word,
    { opacity: [0, 1], y: [20, 0] },
    { duration: 0.5, at: i * 0.1, ease: 'linear' }
  ])
);

scroll(animation, { target, offset: ['start 85%', 'start 20%'] });

With GSAP

Fetta works with any animation library. Here's an example using GSAP.

Animated with GSAP

import { splitText } from 'fetta';
import gsap from 'gsap';

const element = document.querySelector('p');
const { words } = splitText(element);

gsap.from(words, {
  opacity: 0,
  y: 30,
  stagger: 0.05,
  duration: 0.5,
  ease: "power2.out"
});

CSS-Only Animation

Use propIndex to add CSS variables (--char-index, --word-index, --line-index) to each element. Animate entirely with CSS — no JavaScript animation library needed.

Pure CSS animation

import { splitText } from 'fetta';

const element = document.querySelector('p');
splitText(element, { propIndex: true });
.split-char {
  opacity: 0;
  transform: translateY(20px);
  animation: fade-in 0.4s ease forwards;
  animation-delay: calc(var(--char-index) * 0.03s);
}

@keyframes fade-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

On this page