How to Create Text Animation Effects with Anime.js: Split Text and Character Animation Guide

Use the splitText function to wrap text content into animatable <span> elements, then target the resulting character arrays with animate or createTimeline using staggered delays.

The anime library by Julian Garnier provides a dedicated text-splitting engine that transforms static DOM text into hierarchical collections of lines, words, and individual characters. By isolating each character into its own wrapper element, you gain granular control over entrance effects, hover interactions, and complex kinetic typography sequences.

Understanding the splitText API

The splitText factory function, located in src/text/split.js, processes a target element and returns a TextSplitter instance containing arrays of generated wrappers.

Core Parameters and Target Selection

The function accepts a target selector, element, NodeList, or array, followed by a parameters object:

  • chars: Boolean or template string enabling character-level splitting
  • words: Boolean enabling word-level wrapping
  • lines: Boolean enabling line detection based on layout
  • clone: String identifier for creating duplicate elements (used in hover effects)
  • wrap: String specifying overflow behavior ('clip' creates a clipping container)

Template Generation and DOM Structure

The engine uses generateTemplate to construct HTML strings with {value} and {i} placeholders. When processing text nodes, processHTMLTemplate creates DocumentFragment instances and replaces placeholders with actual content:

// Default character template generates:
// <span class="char" data-char="{i}">{value}</span>

const { chars } = splitText('h1', { 
  chars: '<span class="custom-char" data-index="{i}">{value}</span>' 
});

The splitter automatically assigns data-char attributes for stagger indexing and adds aria-hidden="true" to generated spans while preserving a visually-hidden copy of the original text for screen readers.

Creating Character Animation Effects

Once text is split, the chars array contains references to every character span, ready for standard Anime.js animations.

Basic Character Fade-In

Import splitText alongside animate and stagger to create entrance effects:

import { animate, stagger, splitText } from 'animejs';

const { chars } = splitText('#title', { chars: true });

animate(chars, {
  opacity: [0, 1],
  translateY: ['100%', 0],
  duration: 500,
  ease: 'outQuad',
  delay: stagger(30) // 30ms offset per character using data-char attribute
});

Staggered Animations with data-char Attributes

The stagger helper automatically reads the data-char index when you specify the use parameter. This enables complex directional staggering:

animate(chars, {
  scale: [1.5, 1],
  opacity: [0, 1]
}, {
  delay: stagger(50, { 
    from: 'center',
    use: 'data-char' 
  })
});

Advanced Text Splitting Techniques

The splitText engine supports sophisticated patterns for interactive and 3D typography.

Cloning and Hover Effects

Use the clone parameter to duplicate characters for slide-reveal effects. In examples/text/hover-effects/index.js, the implementation creates clipped containers that slide on pointer events:

import { createScope, createTimeline, animate, stagger, splitText } from 'animejs';

createScope({
  root: '#hover-target',
  defaults: { ease: 'outQuad', duration: 500 }
}).add(scope => {
  const { root, methods } = scope;
  
  // Clone chars to the left side with clip overflow
  const { chars } = splitText('h2', {
    chars: { class: 'char', clone: 'left', wrap: 'clip' }
  });

  // Timeline slides the cloned inner span
  const tl = createTimeline({ autoplay: false })
    .add('.char > span', { 
      x: '100%' 
    }, stagger(5, { use: 'data-char' }));

  scope.add('onEnter', () => animate(tl, { progress: 1 }));
  scope.add('onLeave', () => animate(tl, { progress: 0 }));

  root.addEventListener('pointerenter', methods.onEnter);
  root.addEventListener('pointerleave', methods.onLeave);
});

Dynamic Wave Animations

Combine splitText with timeline modifiers to create reactive wave effects that respond to mouse position:

const { chars } = splitText('h2', { chars: true });

const wave = createTimeline({
  autoplay: false,
  loop: true,
  alternate: true
}).add(chars, {
  y: ['-50%', '50%'],
  duration: 500,
  ease: 'inOut(2)',
  modifier: v => v * strength // strength is animated externally
}, stagger(50));

// Animate strength parameter on hover
let strength = 0;
root.addEventListener('pointerenter', () => {
  animate({ strength: 1 }, { 
    onBegin: () => wave.play() 
  });
});

Custom HTML Templates for 3D Effects

Override the default template to inject multiple faces for 3D rotations:

splitText('h2', {
  chars: `<span class="char-3d" data-char="{i}">
           <span class="face front">{value}</span>
           <span class="face back">{value}</span>
         </span>`
});

The {value} placeholder receives the character content, while {i} receives the index. The generated data-char attribute enables standard stagger functionality even with complex nested structures.

Accessibility and Performance Considerations

The TextSplitter class in src/text/split.js implements several optimizations for production use.

Accessibility Features:

  • Each split operation inserts a visually-hidden <span> containing the original HTML before the first line, preserving content for screen readers
  • Generated wrapper elements receive aria-hidden="true" to prevent redundant announcements
  • The original text remains in the DOM for SEO crawlers while the visual presentation uses the split structure

Performance Optimizations:

  • A ResizeObserver monitors the target element's width; when layout changes occur, the splitter automatically re-splits the text while preserving active animation times through the keepTime wrapper
  • The engine reuses a hidden $splitTemplate element to parse HTML templates efficiently
  • Cleanup functions registered via addEffect automatically remove generated DOM elements when calling revert() or when the scope refreshes

Summary

  • Use splitText(target, { chars: true }) to wrap individual characters in animatable <span> elements with automatic data-char indexing
  • Access generated elements through the returned object's chars, words, and lines arrays for direct animation targeting
  • Apply staggered delays with stagger(delay, { use: 'data-char' }) to create sequential character animations without manual loop calculations
  • Implement hover effects using the clone and wrap parameters to create clipped, duplicate characters for slide-reveal interactions
  • Customize markup by passing HTML template strings with {value} and {i} placeholders to build 3D or multi-layer character effects
  • Ensure accessibility by relying on the automatic aria-hidden and visually-hidden original text insertion performed by the splitter engine

Frequently Asked Questions

How does splitText handle international characters and emojis?

The TextSplitter class uses Intl.Segmenter with granularity: 'grapheme' when available to correctly identify user-perceived characters, ensuring that multi-codepoint emojis and accented characters (like é as e + combining acute) are treated as single animatable units. If Intl.Segmenter is unsupported, the engine falls back to spreading the string with [...str], which handles most Unicode cases in modern JavaScript environments.

Can I animate words instead of individual characters?

Yes, set words: true in the parameters object to generate wrappers around whole words while leaving characters unsplit, or combine words: true with chars: true to create a hierarchy of line > word > character elements. The returned object contains separate words and chars arrays, allowing you to animate words with one timeline and characters with another for layered effects.

How do I clean up splitText animations when the component unmounts?

The splitText function automatically registers cleanup logic with the current Scope via addEffect, ensuring that generated DOM elements, event listeners, and the ResizeObserver are removed when you call scope.revert() or when the scope refreshes. For manual cleanup outside a scope, store the splitter instance and call any returned revert functions, or simply remove the target element from the DOM, as the splitter does not maintain external references that would cause memory leaks.

What is the performance impact of splitting large text blocks?

The initial split operation runs a wordSegmenter and graphemeSegmenter over the text content and creates DOM elements for each unit, which has O(n) complexity relative to character count; however, the engine minimizes reflows by building DocumentFragment objects before insertion and uses a ResizeObserver to avoid unnecessary re-splits during animations. For very large documents (thousands of characters), consider splitting only visible viewport text or using words: true instead of chars: true to reduce DOM node count.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →