Text splitting library with kerning compensation for animations
npm install fettaA text-splitting library with kerning compensation for smooth, natural text animations.
Split text into characters, words, and lines while preserving the original typography. Works with any animation library.
- Kerning Compensation — Maintains original character spacing when splitting by chars
- Nested Elements — Preserves , , and other inline elements with all attributes
- Line Detection — Automatically groups words into lines
- Dash Handling — Allows text to wrap naturally after em-dashes, en-dashes, hyphens, and slashes
- Auto Re-split — Re-splits on container resize
- Auto-Revert — Restore original HTML after animations
- Masking — Wrap elements in clip containers for reveal animations
- Emoji Support — Properly handles compound emojis and complex Unicode characters
- Accessible — Automatic screen reader support, even when splitting text with nested links or emphasis
- TypeScript — Full type definitions included
- React Component — Declarative wrapper for React projects
- Built-in InView — Viewport detection for scroll-triggered animations in React
- Library Agnostic — Works with Motion, GSAP, or any animation library
``bash`
npm install fetta
Bundle size: ~3.9 kB (fetta/core) / ~4.8 kB (fetta/react) — minified + compressed
`js
import { splitText } from 'fetta';
import { animate, stagger } from 'motion';
const { chars, words, lines, revert } = splitText(
document.querySelector('h1'),
{ type: 'chars,words,lines' }
);
animate(chars, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.02) });
`
`tsx
import { SplitText } from 'fetta/react';
import { animate, stagger } from 'motion';
function Hero() {
return (
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
}}
>
API
$3
Splits text content into characters, words, and/or lines.
`ts
const result = splitText(element, options);
`#### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
|
type | string | "chars,words,lines" | What to split: "chars", "words", "lines", or combinations |
| charClass | string | "split-char" | CSS class for character elements |
| wordClass | string | "split-word" | CSS class for word elements |
| lineClass | string | "split-line" | CSS class for line elements |
| mask | string | — | Wrap elements in overflow: clip container: "chars", "words", or "lines" |
| autoSplit | boolean | false | Re-split on container resize |
| onResize | function | — | Callback after resize re-split |
| onSplit | function | — | Callback after initial split |
| revertOnComplete | boolean | false | Auto-revert when animation completes |
| propIndex | boolean | false | Add CSS custom properties: --char-index, --word-index, --line-index |
| disableKerning | boolean | false | Skip kerning compensation (no margin adjustments) |
| initialStyles | object | — | Apply initial inline styles to chars/words/lines after split. Values can be objects or (el, index) => object functions |
| initialClasses | object | — | Apply initial CSS classes to chars/words/lines after split. Values can be strings or (el, index) => string functions |#### Return Value
`ts
{
chars: HTMLSpanElement[]; // Character elements
words: HTMLSpanElement[]; // Word elements
lines: HTMLSpanElement[]; // Line elements
revert: () => void; // Restore original HTML and cleanup
}
`$3
`tsx
import { SplitText } from 'fetta/react';
`#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
children | ReactElement | — | Single React element to split |
| as | keyof JSX.IntrinsicElements | "div" | Wrapper element type |
| className | string | — | Class name for wrapper element |
| style | CSSProperties | — | Additional styles for wrapper element |
| ref | Ref | — | Ref to container element |
| onSplit | (result) => void | — | Called after text is split |
| onResize | (result) => void | — | Called on autoSplit re-split |
| options | SplitOptions | — | Split options (type, classes, mask, propIndex, disableKerning) |
| autoSplit | boolean | false | Re-split on container resize |
| revertOnComplete | boolean | false | Revert after animation completes |
| inView | boolean \| InViewOptions | false | Enable viewport detection |
| onInView | (result) => void | — | Called when element enters viewport |
| onLeaveView | (result) => void | — | Called when element leaves viewport |
| initialStyles | object | — | Apply initial inline styles to chars/words/lines. Values can be objects or (el, index) => object functions |
| initialClasses | object | — | Apply initial CSS classes to chars/words/lines. Values can be strings or (el, index) => string functions |
| resetOnLeave | boolean | false | Re-apply initialStyles/initialClasses when leaving viewport |#### Callback Signature
All callbacks (
onSplit, onResize, onInView, onLeaveView) receive the same result object:`ts
{
chars: HTMLSpanElement[];
words: HTMLSpanElement[];
lines: HTMLSpanElement[];
revert: () => void;
}
`#### InView Options
`ts
{
amount?: number; // How much must be visible (0-1), default: 0
margin?: string; // Root margin, default: "0px"
once?: boolean; // Only trigger once, default: false
}
`Examples
$3
#### Basic
`js
import { splitText } from 'fetta';
import { animate, stagger } from 'motion';const { words } = splitText(document.querySelector('h1'));
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
`#### Masked Line Reveal
`js
splitText(element, {
type: 'lines',
mask: 'lines',
onSplit: ({ lines }) => {
animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
}
});
`#### With GSAP
`js
import { splitText } from 'fetta';
import gsap from 'gsap';splitText(element, {
revertOnComplete: true,
onSplit: ({ words }) => {
return gsap.from(words, {
opacity: 0,
y: 20,
stagger: 0.05,
duration: 0.6,
});
}
});
`#### CSS-Only with Index Props
`js
splitText(element, { type: 'chars', propIndex: true });
``css
.split-char {
opacity: 0;
animation: fade-in 0.5s forwards;
animation-delay: calc(var(--char-index) * 0.03s);
}@keyframes fade-in {
to { opacity: 1; }
}
`$3
#### Basic
`tsx
onSplit={({ words }) => {
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
}}
>
Hello World
`#### Masked Line Reveal
`tsx
options={{ type: 'lines', mask: 'lines' }}
onSplit={({ lines }) => {
animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
}}
>
Each line reveals from below
`#### Scroll-Triggered with InView
`tsx
options={{ type: 'words' }}
initialStyles={{
words: { opacity: '0', transform: 'translateY(20px)' }
}}
inView={{ amount: 0.5 }}
onInView={({ words }) => {
animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
}}
resetOnLeave
>
Animates when scrolled into view
`#### Auto-Revert After Animation
`tsx
revertOnComplete
onSplit={({ chars }) => {
return animate(chars, { opacity: [0, 1] }, { delay: stagger(0.02) });
}}
>
Reverts to original HTML after animation
`CSS Classes
Default classes applied to split elements:
| Class | Element | Notes |
|-------|---------|-------|
|
.split-char | Characters | Inline positioning |
| .split-word | Words | Inline positioning |
| .split-line | Lines | Block display |Each element also receives a
data-index attribute with its position.Font Loading
For accurate kerning measurements, fonts must be fully loaded before splitting. When using custom fonts in vanilla JS, wait for
document.fonts.ready:`ts
document.fonts.ready.then(() => {
const { words } = splitText(element);
animate(words, { opacity: [0, 1] });
});
`The React component handles this automatically — no additional setup required.
Accessibility
Fetta automatically handles accessibility to ensure split text remains readable by screen readers.
Headings and landmarks — For elements that support
aria-label natively (headings, ,