fetta 1.5.3 → 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -423
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Text splitting that keeps kerning intact.
|
|
4
4
|
|
|
5
|
-
Split text into
|
|
5
|
+
Split text into chars, words, and lines while preserving original typography. `fetta` provides the framework-agnostic `splitText()` API for vanilla and imperative workflows, `fetta/react` is for React lifecycle and callback-based control, and `fetta/motion` is for Motion-first variants and triggers. `fetta/helpers` adds optional utilities for advanced layered effects.
|
|
6
|
+
|
|
7
|
+
Docs: https://fetta.dimi.me/
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install fetta
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Choose Your Entry Point
|
|
16
|
+
|
|
17
|
+
| Import | Use when | Size (min + brotli) |
|
|
18
|
+
|------|------|------|
|
|
19
|
+
| `fetta` | You want the framework-agnostic core API (`splitText`) | ~7.04 kB |
|
|
20
|
+
| `fetta/react` | You use React and want callback/lifecycle control | ~8.18 kB |
|
|
21
|
+
| `fetta/motion` | You use Motion variants and built-in triggers | ~13.71 kB |
|
|
22
|
+
| `fetta/helpers` | You need optional utilities for layered split effects | ~742 B |
|
|
6
23
|
|
|
7
24
|
## Features
|
|
8
25
|
|
|
@@ -14,186 +31,146 @@ Split text into characters, words, and lines while preserving the original typog
|
|
|
14
31
|
- **Auto Revert** — Restores original HTML after animations
|
|
15
32
|
- **Masking** — Wrap elements in clip containers for reveal animations
|
|
16
33
|
- **Emoji Support** — Properly handles compound emojis and complex Unicode characters
|
|
17
|
-
- **Accessible** — Automatic screen reader support, even
|
|
34
|
+
- **Accessible** — Automatic screen reader support, even with nested links or emphasis
|
|
18
35
|
- **TypeScript** — Full type definitions included
|
|
19
|
-
- **
|
|
20
|
-
- **Viewport Triggers** — Scroll enter/leave callbacks with configurable thresholds in React
|
|
21
|
-
- **Library Agnostic** — Works with Motion, GSAP, CSS, or any animation library
|
|
36
|
+
- **Library Agnostic** — Works with Motion, GSAP, CSS, WAAPI, or custom animation code
|
|
22
37
|
|
|
23
|
-
##
|
|
38
|
+
## Quick Start
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
npm install fetta
|
|
27
|
-
```
|
|
40
|
+
### Vanilla JavaScript (`fetta`)
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `fetta/motion`: ~13.68 kB
|
|
33
|
-
- `fetta/helpers`: ~742 B
|
|
42
|
+
```ts
|
|
43
|
+
import { splitText } from "fetta";
|
|
44
|
+
import { animate, stagger } from "motion";
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
const element = document.querySelector("h1");
|
|
47
|
+
const { chars } = splitText(element, { type: "chars" });
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
animate(chars, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.02) });
|
|
50
|
+
```
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
import { splitText } from 'fetta';
|
|
41
|
-
import { animate, stagger } from 'motion';
|
|
52
|
+
### React Callbacks (`fetta/react`)
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
54
|
+
```tsx
|
|
55
|
+
import { SplitText } from "fetta/react";
|
|
56
|
+
import { animate, stagger } from "motion";
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
<SplitText
|
|
59
|
+
options={{ type: "words" }}
|
|
60
|
+
onSplit={({ words }) => {
|
|
61
|
+
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<h1>Hello World</h1>
|
|
65
|
+
</SplitText>;
|
|
49
66
|
```
|
|
50
67
|
|
|
51
|
-
###
|
|
68
|
+
### Motion Variants (`fetta/motion`)
|
|
52
69
|
|
|
53
70
|
```tsx
|
|
54
|
-
import { SplitText } from
|
|
55
|
-
import {
|
|
71
|
+
import { SplitText } from "fetta/motion";
|
|
72
|
+
import { stagger } from "motion";
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
<SplitText
|
|
75
|
+
variants={{
|
|
76
|
+
hidden: { words: { opacity: 0, y: 20 } },
|
|
77
|
+
visible: { words: { opacity: 1, y: 0 } },
|
|
78
|
+
}}
|
|
79
|
+
initial="hidden"
|
|
80
|
+
animate="visible"
|
|
81
|
+
transition={{ duration: 0.65, delay: stagger(0.04) }}
|
|
82
|
+
options={{ type: "words" }}
|
|
83
|
+
>
|
|
84
|
+
<h1>Hello World</h1>
|
|
85
|
+
</SplitText>;
|
|
68
86
|
```
|
|
69
87
|
|
|
88
|
+
`fetta/motion` supports standard Motion targets, per-type targets (`chars` / `words` / `lines` / `wrapper`), and split-aware function variants.
|
|
89
|
+
|
|
70
90
|
## API
|
|
71
91
|
|
|
72
|
-
### `splitText(element, options?)`
|
|
92
|
+
### `splitText(element, options?)` (`fetta`)
|
|
73
93
|
|
|
74
94
|
Splits text content into characters, words, and/or lines.
|
|
75
95
|
|
|
76
96
|
```ts
|
|
97
|
+
import { splitText } from "fetta";
|
|
98
|
+
|
|
77
99
|
const result = splitText(element, options);
|
|
78
100
|
```
|
|
79
101
|
|
|
80
102
|
#### Options
|
|
81
103
|
|
|
82
104
|
| Option | Type | Default | Description |
|
|
83
|
-
|
|
84
|
-
| `type` | `
|
|
105
|
+
|------|------|------|------|
|
|
106
|
+
| `type` | `SplitType` | `"chars,words,lines"` | What to split: `"chars"`, `"words"`, `"lines"`, or combinations |
|
|
85
107
|
| `charClass` | `string` | `"split-char"` | CSS class for character elements |
|
|
86
108
|
| `wordClass` | `string` | `"split-word"` | CSS class for word elements |
|
|
87
109
|
| `lineClass` | `string` | `"split-line"` | CSS class for line elements |
|
|
88
|
-
| `mask` | `
|
|
110
|
+
| `mask` | `"chars" \| "words" \| "lines"` | — | Wrap elements in `overflow: clip` container |
|
|
89
111
|
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
90
|
-
| `onResplit` | `
|
|
91
|
-
| `onSplit` | `
|
|
92
|
-
| `revertOnComplete` | `boolean` | `false` | Auto-revert when animation completes |
|
|
112
|
+
| `onResplit` | `(result) => void` | — | Callback after autoSplit/full-resplit replaces split output elements |
|
|
113
|
+
| `onSplit` | `(result) => CallbackReturn` | — | Callback after initial split. Return animation/promise for `revertOnComplete` |
|
|
114
|
+
| `revertOnComplete` | `boolean` | `false` | Auto-revert when returned animation completes |
|
|
93
115
|
| `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
|
|
94
116
|
| `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
|
|
95
117
|
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split. Values can be objects or `(el, index) => object` functions |
|
|
96
118
|
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values are strings |
|
|
97
119
|
|
|
98
|
-
#### Return Value
|
|
99
|
-
|
|
100
120
|
```ts
|
|
101
|
-
|
|
102
|
-
chars
|
|
103
|
-
words
|
|
104
|
-
lines
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
type SplitType =
|
|
122
|
+
| "chars"
|
|
123
|
+
| "words"
|
|
124
|
+
| "lines"
|
|
125
|
+
| "chars,words"
|
|
126
|
+
| "words,lines"
|
|
127
|
+
| "chars,lines"
|
|
128
|
+
| "chars,words,lines";
|
|
107
129
|
```
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Builds swap/reveal DOM layers (clones + optional wrappers) without coupling to any animation library.
|
|
131
|
+
#### Return Value
|
|
112
132
|
|
|
113
133
|
```ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Animate with Motion, GSAP, WAAPI, or CSS
|
|
121
|
-
// ...
|
|
122
|
-
|
|
123
|
-
layers.cleanup(); // removes clones/wrappers, keeps split DOM
|
|
124
|
-
// layers.cleanup({ revertSplit: true }) // also calls split.revert()
|
|
134
|
+
interface SplitTextResult {
|
|
135
|
+
chars: HTMLSpanElement[];
|
|
136
|
+
words: HTMLSpanElement[];
|
|
137
|
+
lines: HTMLSpanElement[];
|
|
138
|
+
revert: () => void;
|
|
139
|
+
}
|
|
125
140
|
```
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- Clone is always appended to the **current parent** of the original split node.
|
|
130
|
-
- `wrap: false` (default): clone is appended to existing parent (often the mask wrapper).
|
|
131
|
-
- `wrap: true`: original is first moved into a track wrapper, then clone is appended there.
|
|
132
|
-
- Helper never calls `splitText` and never performs animation.
|
|
133
|
-
|
|
134
|
-
#### Options
|
|
135
|
-
|
|
136
|
-
| Option | Type | Default | Description |
|
|
137
|
-
|--------|------|---------|-------------|
|
|
138
|
-
| `unit` | `"chars" \| "words" \| "lines"` | — | Which split nodes to layer |
|
|
139
|
-
| `wrap` | `boolean` | `false` | Wrap each original in a track wrapper (`position: relative`) |
|
|
140
|
-
| `display` | `"auto" \| "inline-block" \| "block"` | `"auto"` | Track display when `wrap: true` (`lines` => `block`, others => `inline-block`) |
|
|
141
|
-
| `cloneOffset.axis` | `"x" \| "y"` | `"y"` | Axis used for initial clone offset |
|
|
142
|
-
| `cloneOffset.direction` | `"start" \| "end"` | `"start"` | Offset direction (`start` => negative) |
|
|
143
|
-
| `cloneOffset.distance` | `string` | `"100%"` | Offset distance |
|
|
144
|
-
| `trackClassName` / `cloneClassName` | `string \| (ctx) => string \| undefined` | — | Class names (static or per-item) |
|
|
145
|
-
| `trackStyle` / `cloneStyle` | `object \| (ctx) => object` | — | Inline styles (static or per-item) |
|
|
146
|
-
|
|
147
|
-
For reveal/swap effects, use matching `mask` in `splitText` (`"chars"`, `"words"`, or `"lines"`).
|
|
148
|
-
|
|
149
|
-
### `<SplitText>` (React)
|
|
142
|
+
### `<SplitText>` (`fetta/react`)
|
|
150
143
|
|
|
151
144
|
```tsx
|
|
152
|
-
import { SplitText } from
|
|
145
|
+
import { SplitText } from "fetta/react";
|
|
153
146
|
```
|
|
154
147
|
|
|
155
|
-
`fetta/react`
|
|
156
|
-
|
|
157
|
-
`fetta/react` props:
|
|
148
|
+
`fetta/react` wraps `splitText()` for React with lifecycle hooks, viewport callbacks, and automatic cleanup.
|
|
158
149
|
|
|
159
150
|
#### React Props
|
|
160
151
|
|
|
161
152
|
| Prop | Type | Default | Description |
|
|
162
|
-
|
|
153
|
+
|------|------|------|------|
|
|
163
154
|
| `children` | `ReactElement` | — | Single React element to split |
|
|
164
155
|
| `as` | `keyof JSX.IntrinsicElements` | `"div"` | Wrapper element type |
|
|
165
|
-
| `className` | `string` | — |
|
|
166
|
-
| `style` | `CSSProperties` | — |
|
|
167
|
-
| `ref` | `Ref<HTMLElement>` | — | Ref to
|
|
168
|
-
| `onSplit` | `(result) => CallbackReturn` | — | Called after
|
|
156
|
+
| `className` | `string` | — | Wrapper class name |
|
|
157
|
+
| `style` | `CSSProperties` | — | Wrapper styles |
|
|
158
|
+
| `ref` | `Ref<HTMLElement>` | — | Ref to wrapper element |
|
|
159
|
+
| `onSplit` | `(result) => CallbackReturn` | — | Called after initial split |
|
|
169
160
|
| `onResplit` | `(result) => void` | — | Called when autoSplit/full-resplit replaces split output elements |
|
|
170
|
-
| `options` | `SplitTextOptions` | — | Split options (type
|
|
161
|
+
| `options` | `SplitTextOptions` | — | Split options (`type`, classes, mask, etc.) |
|
|
171
162
|
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
172
|
-
| `waitForFonts` | `boolean` | `true` | Wait for `document.fonts.ready` before splitting
|
|
163
|
+
| `waitForFonts` | `boolean` | `true` | Wait for `document.fonts.ready` before splitting |
|
|
173
164
|
| `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
|
|
174
|
-
| `onRevert` | `() => void` | — | Called when split text
|
|
165
|
+
| `onRevert` | `() => void` | — | Called when split text reverts |
|
|
175
166
|
| `viewport` | `ViewportOptions` | — | Configure viewport detection |
|
|
176
|
-
| `onViewportEnter` | `(result) => CallbackReturn` | — | Called when
|
|
177
|
-
| `onViewportLeave` | `(result) => CallbackReturn` | — | Called when
|
|
178
|
-
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines
|
|
179
|
-
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines
|
|
180
|
-
| `resetOnViewportLeave` | `boolean` | `false` | Re-apply
|
|
167
|
+
| `onViewportEnter` | `(result) => CallbackReturn` | — | Called when entering viewport |
|
|
168
|
+
| `onViewportLeave` | `(result) => CallbackReturn` | — | Called when leaving viewport |
|
|
169
|
+
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines |
|
|
170
|
+
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines |
|
|
171
|
+
| `resetOnViewportLeave` | `boolean` | `false` | Re-apply initial styles/classes when leaving viewport |
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
| Option | Type | Default | Description |
|
|
185
|
-
|------|------|---------|-------------|
|
|
186
|
-
| `type` | `SplitType` | `"chars,words,lines"` | What to split: `"chars"`, `"words"`, `"lines"`, or combinations |
|
|
187
|
-
| `charClass` | `string` | `"split-char"` | CSS class for character spans |
|
|
188
|
-
| `wordClass` | `string` | `"split-word"` | CSS class for word spans |
|
|
189
|
-
| `lineClass` | `string` | `"split-line"` | CSS class for line spans |
|
|
190
|
-
| `mask` | `"lines" \| "words" \| "chars"` | — | Wrap elements in `overflow: clip` mask containers |
|
|
191
|
-
| `propIndex` | `boolean` | `false` | Add CSS index variables (`--char-index`, `--word-index`, `--line-index`) |
|
|
192
|
-
| `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
|
|
193
|
-
|
|
194
|
-
#### Callback Signature
|
|
195
|
-
|
|
196
|
-
All callbacks (`onSplit`, `onResplit`, `onViewportEnter`, `onViewportLeave`) receive:
|
|
173
|
+
All callbacks receive:
|
|
197
174
|
|
|
198
175
|
```ts
|
|
199
176
|
{
|
|
@@ -213,314 +190,118 @@ type CallbackReturn =
|
|
|
213
190
|
| CallbackReturn[];
|
|
214
191
|
```
|
|
215
192
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
`onRevert` is a separate zero-argument callback that fires when a split cycle actually reverts.
|
|
219
|
-
|
|
220
|
-
#### Viewport Options
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
{
|
|
224
|
-
amount?: number | "some" | "all"; // Enter threshold, default: 0
|
|
225
|
-
leave?: number | "some" | "all"; // Leave threshold, default: 0
|
|
226
|
-
margin?: string; // Root margin, default: "0px"
|
|
227
|
-
once?: boolean; // Only trigger once, default: false
|
|
228
|
-
root?: RefObject<Element>; // Optional root element
|
|
229
|
-
}
|
|
230
|
-
```
|
|
193
|
+
`fetta/react` and `fetta/motion` both forward common wrapper DOM props (`id`, `role`, `tabIndex`, `aria-*`, `data-*`, event handlers) to the wrapper.
|
|
231
194
|
|
|
232
|
-
### `<SplitText>` (
|
|
195
|
+
### `<SplitText>` (`fetta/motion`)
|
|
233
196
|
|
|
234
197
|
```tsx
|
|
235
198
|
import { SplitText } from "fetta/motion";
|
|
236
199
|
```
|
|
237
200
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Animate on exit with Motion's `AnimatePresence` (make `SplitText` the direct child):
|
|
241
|
-
|
|
242
|
-
```tsx
|
|
243
|
-
import { AnimatePresence } from "motion/react";
|
|
244
|
-
|
|
245
|
-
<AnimatePresence>
|
|
246
|
-
{isVisible && (
|
|
247
|
-
<SplitText
|
|
248
|
-
variants={{
|
|
249
|
-
enter: { opacity: 1, y: 0 },
|
|
250
|
-
exit: { opacity: 0, y: 12 },
|
|
251
|
-
}}
|
|
252
|
-
initial="enter"
|
|
253
|
-
animate="enter"
|
|
254
|
-
exit="exit"
|
|
255
|
-
options={{ type: "words" }}
|
|
256
|
-
>
|
|
257
|
-
<h1>Goodbye</h1>
|
|
258
|
-
</SplitText>
|
|
259
|
-
)}
|
|
260
|
-
</AnimatePresence>
|
|
261
|
-
```
|
|
201
|
+
Variant-driven component built on Motion. Includes every `fetta/react` prop plus Motion animation/triggers (`initial`, `animate`, `exit`, `whileInView`, `whileScroll`, `whileHover`, etc.).
|
|
262
202
|
|
|
263
203
|
#### Motion-only Props
|
|
264
204
|
|
|
265
205
|
| Prop | Type | Default | Description |
|
|
266
|
-
|
|
206
|
+
|------|------|------|------|
|
|
267
207
|
| `variants` | `Record<string, VariantDefinition<TCustom>>` | — | Named variant definitions |
|
|
268
208
|
| `initial` | `string \| VariantDefinition<TCustom> \| false` | — | Initial variant applied instantly after split |
|
|
269
209
|
| `animate` | `string \| VariantDefinition<TCustom>` | — | Base variant |
|
|
270
|
-
| `exit` | `string \| VariantDefinition<TCustom> \| false` | — | Exit variant (AnimatePresence) |
|
|
210
|
+
| `exit` | `string \| VariantDefinition<TCustom> \| false` | — | Exit variant (`AnimatePresence`) |
|
|
271
211
|
| `whileInView` | `string \| VariantDefinition<TCustom>` | — | Variant while element is in view |
|
|
272
212
|
| `whileOutOfView` | `string \| VariantDefinition<TCustom>` | — | Variant after element leaves view |
|
|
273
213
|
| `whileScroll` | `string \| VariantDefinition<TCustom>` | — | Scroll-driven variant (highest trigger priority) |
|
|
274
214
|
| `whileHover` | `string \| VariantDefinition<TCustom>` | — | Variant on hover |
|
|
275
215
|
| `whileTap` | `string \| VariantDefinition<TCustom>` | — | Variant on tap/press |
|
|
276
216
|
| `whileFocus` | `string \| VariantDefinition<TCustom>` | — | Variant on focus |
|
|
277
|
-
| `animateOnResplit` | `boolean` | `false` | Replay initial->animate on autoSplit/full-resplit |
|
|
217
|
+
| `animateOnResplit` | `boolean` | `false` | Replay `initial -> animate` on autoSplit/full-resplit |
|
|
278
218
|
| `scroll` | `{ offset?, axis?, container? }` | — | Scroll tracking options for `whileScroll` |
|
|
279
219
|
| `transition` | `AnimationOptions` | — | Global/default transition for variants |
|
|
280
220
|
| `custom` | `TCustom` | — | Custom data forwarded to function variants |
|
|
281
|
-
| `delayScope` | `"global" \| "local"` | `"global"` | Delay-function index scope (`globalIndex` vs
|
|
282
|
-
| `reducedMotion` | `"user" \| "always" \| "never"` |
|
|
221
|
+
| `delayScope` | `"global" \| "local"` | `"global"` | Delay-function index scope (`globalIndex` vs local `index`) |
|
|
222
|
+
| `reducedMotion` | `"user" \| "always" \| "never"` | `"never"` | Reduced-motion behavior for this component |
|
|
283
223
|
| `onHoverStart` | `() => void` | — | Called when hover starts |
|
|
284
224
|
| `onHoverEnd` | `() => void` | — | Called when hover ends |
|
|
285
225
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
### Vanilla JavaScript
|
|
289
|
-
|
|
290
|
-
#### Basic
|
|
291
|
-
|
|
292
|
-
```js
|
|
293
|
-
import { splitText } from 'fetta';
|
|
294
|
-
import { animate, stagger } from 'motion';
|
|
295
|
-
|
|
296
|
-
const { words } = splitText(document.querySelector('h1'));
|
|
297
|
-
|
|
298
|
-
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
#### Masked Line Reveal
|
|
302
|
-
|
|
303
|
-
```js
|
|
304
|
-
splitText(element, {
|
|
305
|
-
type: 'lines',
|
|
306
|
-
mask: 'lines',
|
|
307
|
-
onSplit: ({ lines }) => {
|
|
308
|
-
animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
#### With GSAP
|
|
314
|
-
|
|
315
|
-
```js
|
|
316
|
-
import { splitText } from 'fetta';
|
|
317
|
-
import gsap from 'gsap';
|
|
318
|
-
|
|
319
|
-
splitText(element, {
|
|
320
|
-
revertOnComplete: true,
|
|
321
|
-
onSplit: ({ words }) => {
|
|
322
|
-
return gsap.from(words, {
|
|
323
|
-
opacity: 0,
|
|
324
|
-
y: 20,
|
|
325
|
-
stagger: 0.05,
|
|
326
|
-
duration: 0.6,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
#### CSS-Only with Index Props
|
|
333
|
-
|
|
334
|
-
```js
|
|
335
|
-
splitText(element, { type: 'chars', propIndex: true });
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
```css
|
|
339
|
-
.split-char {
|
|
340
|
-
opacity: 0;
|
|
341
|
-
animation: fade-in 0.5s forwards;
|
|
342
|
-
animation-delay: calc(var(--char-index) * 0.03s);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
@keyframes fade-in {
|
|
346
|
-
to { opacity: 1; }
|
|
347
|
-
}
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### React
|
|
351
|
-
|
|
352
|
-
#### Basic
|
|
353
|
-
|
|
354
|
-
```tsx
|
|
355
|
-
<SplitText
|
|
356
|
-
onSplit={({ words }) => {
|
|
357
|
-
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
358
|
-
}}
|
|
359
|
-
>
|
|
360
|
-
<h1>Hello World</h1>
|
|
361
|
-
</SplitText>
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
#### Masked Line Reveal
|
|
365
|
-
|
|
366
|
-
```tsx
|
|
367
|
-
<SplitText
|
|
368
|
-
options={{ type: 'lines', mask: 'lines' }}
|
|
369
|
-
onSplit={({ lines }) => {
|
|
370
|
-
animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
|
|
371
|
-
}}
|
|
372
|
-
>
|
|
373
|
-
<p>Each line reveals from below</p>
|
|
374
|
-
</SplitText>
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
#### Scroll-Triggered with Viewport
|
|
226
|
+
Exit animations use standard Motion behavior (`SplitText` must be a direct child of `AnimatePresence`):
|
|
378
227
|
|
|
379
228
|
```tsx
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
initialStyles={{
|
|
383
|
-
words: { opacity: '0', transform: 'translateY(20px)' }
|
|
384
|
-
}}
|
|
385
|
-
viewport={{ amount: 0.5 }}
|
|
386
|
-
onViewportEnter={({ words }) => {
|
|
387
|
-
animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
|
|
388
|
-
}}
|
|
389
|
-
resetOnViewportLeave
|
|
390
|
-
>
|
|
391
|
-
<p>Animates when scrolled into view</p>
|
|
392
|
-
</SplitText>
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
#### Auto-Revert After Animation
|
|
229
|
+
import { AnimatePresence } from "motion/react";
|
|
230
|
+
import { SplitText } from "fetta/motion";
|
|
396
231
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
232
|
+
<AnimatePresence>
|
|
233
|
+
{isVisible && (
|
|
234
|
+
<SplitText
|
|
235
|
+
variants={{
|
|
236
|
+
enter: { opacity: 1, y: 0 },
|
|
237
|
+
exit: { opacity: 0, y: 12 },
|
|
238
|
+
}}
|
|
239
|
+
initial="enter"
|
|
240
|
+
animate="enter"
|
|
241
|
+
exit="exit"
|
|
242
|
+
options={{ type: "words" }}
|
|
243
|
+
>
|
|
244
|
+
<h1>Goodbye</h1>
|
|
245
|
+
</SplitText>
|
|
246
|
+
)}
|
|
247
|
+
</AnimatePresence>;
|
|
406
248
|
```
|
|
407
249
|
|
|
408
|
-
###
|
|
250
|
+
### `createSplitClones(splitResult, options)` (`fetta/helpers`, optional)
|
|
409
251
|
|
|
410
|
-
|
|
252
|
+
Helpers are optional utilities for advanced layered reveal/swap effects.
|
|
411
253
|
|
|
412
|
-
```
|
|
413
|
-
import {
|
|
414
|
-
import {
|
|
415
|
-
|
|
416
|
-
<SplitText
|
|
417
|
-
variants={{
|
|
418
|
-
hidden: { opacity: 0, y: 20, filter: 'blur(6px)' },
|
|
419
|
-
visible: { opacity: 1, y: 0, filter: 'blur(0px)' },
|
|
420
|
-
}}
|
|
421
|
-
initial="hidden"
|
|
422
|
-
animate="visible"
|
|
423
|
-
transition={{ duration: 0.6, delay: stagger(0.04) }}
|
|
424
|
-
options={{ type: 'words' }}
|
|
425
|
-
>
|
|
426
|
-
<h1>Hello World</h1>
|
|
427
|
-
</SplitText>
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
#### Line-Aware Stagger
|
|
431
|
-
|
|
432
|
-
```tsx
|
|
433
|
-
import { SplitText } from 'fetta/motion';
|
|
434
|
-
import { stagger } from 'motion';
|
|
254
|
+
```ts
|
|
255
|
+
import { splitText } from "fetta";
|
|
256
|
+
import { createSplitClones } from "fetta/helpers";
|
|
435
257
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
variants={{
|
|
439
|
-
hidden: { chars: { opacity: 0 } },
|
|
440
|
-
visible: {
|
|
441
|
-
chars: ({ lineIndex }) => ({
|
|
442
|
-
opacity: 1,
|
|
443
|
-
transition: {
|
|
444
|
-
duration: 0.3,
|
|
445
|
-
delay: stagger(0.015, {
|
|
446
|
-
startDelay: lineIndex * 0.2,
|
|
447
|
-
from: lineIndex % 2 === 0 ? "first" : "last",
|
|
448
|
-
}),
|
|
449
|
-
},
|
|
450
|
-
}),
|
|
451
|
-
},
|
|
452
|
-
}}
|
|
453
|
-
initial="hidden"
|
|
454
|
-
animate="visible"
|
|
455
|
-
options={{ type: "chars,lines" }}
|
|
456
|
-
>
|
|
457
|
-
<p>Line-aware per-character animation</p>
|
|
458
|
-
</SplitText>
|
|
258
|
+
const split = splitText(element, { type: "chars", mask: "chars" });
|
|
259
|
+
const layers = createSplitClones(split, { unit: "chars", wrap: true });
|
|
459
260
|
```
|
|
460
261
|
|
|
461
|
-
####
|
|
462
|
-
|
|
463
|
-
```tsx
|
|
464
|
-
import { SplitText } from 'fetta/motion';
|
|
262
|
+
#### Helper Options
|
|
465
263
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}),
|
|
477
|
-
}}
|
|
478
|
-
scroll={{ offset: ["start 90%", "start 10%"] }}
|
|
479
|
-
options={{ type: "chars" }}
|
|
480
|
-
>
|
|
481
|
-
<p>Characters fade in with scroll progress</p>
|
|
482
|
-
</SplitText>
|
|
483
|
-
```
|
|
264
|
+
| Option | Type | Default | Description |
|
|
265
|
+
|------|------|------|------|
|
|
266
|
+
| `unit` | `"chars" \| "words" \| "lines"` | — | Which split nodes to layer |
|
|
267
|
+
| `wrap` | `boolean` | `false` | Wrap each original in a track wrapper (`position: relative`) |
|
|
268
|
+
| `display` | `"auto" \| "inline-block" \| "block"` | `"auto"` | Track display when `wrap: true` (`lines` => `block`, others => `inline-block`) |
|
|
269
|
+
| `cloneOffset.axis` | `"x" \| "y"` | `"y"` | Axis used for initial clone offset |
|
|
270
|
+
| `cloneOffset.direction` | `"start" \| "end"` | `"start"` | Offset direction (`start` => negative) |
|
|
271
|
+
| `cloneOffset.distance` | `string` | `"100%"` | Offset distance |
|
|
272
|
+
| `trackClassName` / `cloneClassName` | `string \| (ctx) => string \| undefined` | — | Class names (static or per-item) |
|
|
273
|
+
| `trackStyle` / `cloneStyle` | `object \| (ctx) => object` | — | Inline styles (static or per-item) |
|
|
484
274
|
|
|
485
|
-
####
|
|
275
|
+
#### Helper Behavior
|
|
486
276
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
277
|
+
- Helper does not call `splitText()`; pass an existing split result.
|
|
278
|
+
- Clone is appended to the current parent of each original split node.
|
|
279
|
+
- `wrap: false` appends clone to existing parent.
|
|
280
|
+
- `wrap: true` moves original into a generated track, then appends clone there.
|
|
281
|
+
- `cleanup()` removes helper-created tracks/clones and is idempotent.
|
|
282
|
+
- `cleanup({ revertSplit: true })` also calls `split.revert()`.
|
|
490
283
|
|
|
491
|
-
|
|
492
|
-
variants={{
|
|
493
|
-
rest: { chars: { opacity: 0.85, y: 0 } },
|
|
494
|
-
hover: { chars: { opacity: 1, y: -6 } },
|
|
495
|
-
}}
|
|
496
|
-
initial="rest"
|
|
497
|
-
animate="rest"
|
|
498
|
-
whileHover="hover"
|
|
499
|
-
transition={{ duration: 0.25, delay: stagger(0.01) }}
|
|
500
|
-
options={{ type: 'chars' }}
|
|
501
|
-
>
|
|
502
|
-
<p>Hover this text</p>
|
|
503
|
-
</SplitText>
|
|
504
|
-
```
|
|
284
|
+
For reveal/swap effects, match helper `unit` with `splitText` `mask` (`"chars"`, `"words"`, `"lines"`).
|
|
505
285
|
|
|
506
286
|
## CSS Classes
|
|
507
287
|
|
|
508
|
-
Default classes applied to split
|
|
288
|
+
Default classes applied to split output:
|
|
509
289
|
|
|
510
|
-
| Class | Element |
|
|
511
|
-
|
|
512
|
-
| `.split-char` |
|
|
513
|
-
| `.split-word` |
|
|
514
|
-
| `.split-line` |
|
|
290
|
+
| Class | Element |
|
|
291
|
+
|------|------|
|
|
292
|
+
| `.split-char` | Character span |
|
|
293
|
+
| `.split-word` | Word span |
|
|
294
|
+
| `.split-line` | Line span |
|
|
515
295
|
|
|
516
|
-
Split elements receive
|
|
517
|
-
|
|
518
|
-
-
|
|
519
|
-
-
|
|
296
|
+
Split elements receive index attributes:
|
|
297
|
+
|
|
298
|
+
- `data-char-index`
|
|
299
|
+
- `data-word-index`
|
|
300
|
+
- `data-line-index`
|
|
520
301
|
|
|
521
302
|
## Font Loading
|
|
522
303
|
|
|
523
|
-
For
|
|
304
|
+
For stable kerning measurements in vanilla usage, wait for fonts before splitting:
|
|
524
305
|
|
|
525
306
|
```ts
|
|
526
307
|
document.fonts.ready.then(() => {
|
|
@@ -529,63 +310,44 @@ document.fonts.ready.then(() => {
|
|
|
529
310
|
});
|
|
530
311
|
```
|
|
531
312
|
|
|
532
|
-
React and Motion components wait for fonts by default (`waitForFonts={true}`)
|
|
533
|
-
|
|
534
|
-
If you notice a visual shift after splitting, keep the default waiting behavior enabled.
|
|
535
|
-
|
|
536
|
-
If you need immediate splitting (for example, responsiveness-first UI), you can opt out with `waitForFonts={false}`:
|
|
537
|
-
|
|
538
|
-
```tsx
|
|
539
|
-
<SplitText waitForFonts={false}>
|
|
540
|
-
<h1>Split Immediately</h1>
|
|
541
|
-
</SplitText>
|
|
542
|
-
```
|
|
313
|
+
React and Motion components wait for fonts by default (`waitForFonts={true}`).
|
|
543
314
|
|
|
544
315
|
## Accessibility
|
|
545
316
|
|
|
546
|
-
Fetta
|
|
547
|
-
|
|
548
|
-
**Headings and landmarks** — For elements that support `aria-label` natively (headings, `<section>`, `<nav>`, etc.), Fetta adds `aria-hidden="true"` to each split span and an `aria-label` on the parent:
|
|
317
|
+
Fetta keeps split text readable by screen readers:
|
|
549
318
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
<span class="split-word" aria-hidden="true">Hello</span>
|
|
554
|
-
<span class="split-word" aria-hidden="true">World</span>
|
|
555
|
-
</h1>
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**Generic elements and nested content** — For `<span>`, `<div>`, `<p>`, or text containing inline elements like links, Fetta wraps the visual content with `aria-hidden="true"` and adds a screen-reader-only copy that preserves the semantic structure:
|
|
559
|
-
|
|
560
|
-
```html
|
|
561
|
-
<!-- After splitting <p>Click <a href="/signup">here</a> to start</p> -->
|
|
562
|
-
<p>
|
|
563
|
-
<span aria-hidden="true" data-fetta-visual="true">
|
|
564
|
-
<!-- Split visual content -->
|
|
565
|
-
</span>
|
|
566
|
-
<span class="fetta-sr-only" data-fetta-sr-copy="true">
|
|
567
|
-
Click <a href="/signup">here</a> to start
|
|
568
|
-
</span>
|
|
569
|
-
</p>
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
Pre-existing `aria-label` attributes are always preserved.
|
|
319
|
+
- Headings/landmarks: split nodes are hidden from assistive tech and parent gets `aria-label`.
|
|
320
|
+
- Generic/nested content: visual split output is hidden and a screen-reader copy preserves semantics.
|
|
321
|
+
- Existing `aria-label` values are preserved.
|
|
573
322
|
|
|
574
323
|
## Notes
|
|
575
324
|
|
|
576
|
-
-
|
|
577
|
-
-
|
|
325
|
+
- Ligatures are disabled (`font-variant-ligatures: none`) because ligatures cannot span multiple elements.
|
|
326
|
+
- Authored hard breaks are preserved (`<br>` and block boundaries are kept as hard split boundaries).
|
|
578
327
|
|
|
579
328
|
## Browser Support
|
|
580
329
|
|
|
581
330
|
All modern browsers: Chrome, Firefox, Safari, Edge.
|
|
582
331
|
|
|
583
332
|
Requires:
|
|
333
|
+
|
|
584
334
|
- `ResizeObserver`
|
|
585
335
|
- `IntersectionObserver`
|
|
586
336
|
- `Intl.Segmenter`
|
|
587
337
|
|
|
588
|
-
|
|
338
|
+
Safari kerning compensation works, but font rendering precision can vary slightly by font. If you notice subtle shifts around `revert()`, use `disableKerning: true`.
|
|
339
|
+
|
|
340
|
+
## Docs
|
|
341
|
+
|
|
342
|
+
- https://fetta.dimi.me/
|
|
343
|
+
- https://fetta.dimi.me/installation
|
|
344
|
+
- https://fetta.dimi.me/api/core
|
|
345
|
+
- https://fetta.dimi.me/api/react
|
|
346
|
+
- https://fetta.dimi.me/api/motion
|
|
347
|
+
- https://fetta.dimi.me/api/helpers
|
|
348
|
+
- https://fetta.dimi.me/examples/vanilla
|
|
349
|
+
- https://fetta.dimi.me/examples/react
|
|
350
|
+
- https://fetta.dimi.me/examples/motion
|
|
589
351
|
|
|
590
352
|
## License
|
|
591
353
|
|