fetta 1.0.0
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/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/chunk-KGMU2B53.js +764 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +1 -0
- package/dist/react.d.ts +121 -0
- package/dist/react.js +181 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# fetta
|
|
2
|
+
|
|
3
|
+
A text-splitting library with kerning compensation for smooth, natural text animations.
|
|
4
|
+
|
|
5
|
+
Split text into characters, words, and lines while preserving the original typography. Works with any animation library.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Kerning Compensation** — Measures character positions before splitting, then applies margin adjustments to maintain original spacing
|
|
10
|
+
- **Nested Element Support** — Preserves inline HTML elements (`<a>`, `<em>`, `<strong>`, etc.) with all attributes intact
|
|
11
|
+
- **Natural Line Wrapping** — Detects lines based on Y-position clustering, works with any container width
|
|
12
|
+
- **Dash Handling** — Allows text to wrap naturally after em-dashes, en-dashes, and hyphens
|
|
13
|
+
- **Auto Re-split** — Automatically re-splits on container resize with debouncing
|
|
14
|
+
- **Viewport Detection** — Built-in IntersectionObserver support for scroll-triggered animations
|
|
15
|
+
- **Masking** — Wrap elements in clip containers for reveal animations
|
|
16
|
+
- **Animation Agnostic** — Works with Motion, GSAP, or any animation library
|
|
17
|
+
- **React Component** — First-class React support with hooks and cleanup
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install fetta
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### Vanilla JavaScript
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { splitText } from 'fetta';
|
|
31
|
+
import { animate, stagger } from 'motion';
|
|
32
|
+
|
|
33
|
+
const { chars, words, lines, revert } = splitText(
|
|
34
|
+
document.querySelector('h1'),
|
|
35
|
+
{ type: 'chars,words,lines' }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
animate(chars, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.02) });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### React
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { SplitText } from 'fetta/react';
|
|
45
|
+
import { animate, stagger } from 'motion';
|
|
46
|
+
|
|
47
|
+
function Hero() {
|
|
48
|
+
return (
|
|
49
|
+
<SplitText
|
|
50
|
+
onSplit={({ words }) => {
|
|
51
|
+
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<h1>Hello World</h1>
|
|
55
|
+
</SplitText>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### `splitText(element, options?)`
|
|
63
|
+
|
|
64
|
+
Splits text content into characters, words, and/or lines.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const result = splitText(element, options);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Options
|
|
71
|
+
|
|
72
|
+
| Option | Type | Default | Description |
|
|
73
|
+
|--------|------|---------|-------------|
|
|
74
|
+
| `type` | `string` | `"chars,words,lines"` | What to split: `"chars"`, `"words"`, `"lines"`, or combinations |
|
|
75
|
+
| `charClass` | `string` | `"split-char"` | CSS class for character elements |
|
|
76
|
+
| `wordClass` | `string` | `"split-word"` | CSS class for word elements |
|
|
77
|
+
| `lineClass` | `string` | `"split-line"` | CSS class for line elements |
|
|
78
|
+
| `mask` | `"lines" \| "words" \| "chars"` | — | Wraps elements in `overflow: clip` container for reveal animations |
|
|
79
|
+
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
80
|
+
| `onResize` | `function` | — | Callback after resize re-split |
|
|
81
|
+
| `onSplit` | `function` | — | Callback after initial split |
|
|
82
|
+
| `revertOnComplete` | `boolean` | `false` | Auto-revert when animation completes |
|
|
83
|
+
| `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
|
|
84
|
+
| `willChange` | `boolean` | `false` | Add `will-change: transform, opacity` for performance |
|
|
85
|
+
|
|
86
|
+
#### Return Value
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
{
|
|
90
|
+
chars: HTMLSpanElement[]; // Character elements
|
|
91
|
+
words: HTMLSpanElement[]; // Word elements
|
|
92
|
+
lines: HTMLSpanElement[]; // Line elements
|
|
93
|
+
revert: () => void; // Restore original HTML and cleanup
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `<SplitText>` (React)
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { SplitText } from 'fetta/react';
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Props
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Default | Description |
|
|
106
|
+
|------|------|---------|-------------|
|
|
107
|
+
| `children` | `ReactElement` | — | Single element to split |
|
|
108
|
+
| `onSplit` | `function` | — | Called after text is split |
|
|
109
|
+
| `onResize` | `function` | — | Called on autoSplit re-split |
|
|
110
|
+
| `options` | `object` | — | Split options (type, classes, mask, propIndex, willChange) |
|
|
111
|
+
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
112
|
+
| `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
|
|
113
|
+
| `inView` | `boolean \| InViewOptions` | `false` | Enable viewport detection |
|
|
114
|
+
| `onInView` | `function` | — | Called when element enters viewport |
|
|
115
|
+
| `onLeaveView` | `function` | — | Called when element leaves viewport |
|
|
116
|
+
|
|
117
|
+
#### InView Options
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
{
|
|
121
|
+
amount?: number; // How much must be visible (0-1), default: 0
|
|
122
|
+
margin?: string; // Root margin, default: "0px"
|
|
123
|
+
once?: boolean; // Only trigger once, default: false
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
### Masked Line Reveal
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<SplitText
|
|
133
|
+
options={{ type: 'lines', mask: 'lines' }}
|
|
134
|
+
onSplit={({ lines }) => {
|
|
135
|
+
animate(lines, { y: ['100%', 0] }, { delay: stagger(0.1) });
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<p>Each line reveals from below</p>
|
|
139
|
+
</SplitText>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Scroll-Triggered Animation
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
<SplitText
|
|
146
|
+
onSplit={({ words }) => {
|
|
147
|
+
words.forEach(w => (w.style.opacity = '0'));
|
|
148
|
+
}}
|
|
149
|
+
inView={{ amount: 0.5, once: true }}
|
|
150
|
+
onInView={({ words }) => {
|
|
151
|
+
animate(words, { opacity: 1, y: [20, 0] }, { delay: stagger(0.03) });
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<p>Animates when scrolled into view</p>
|
|
155
|
+
</SplitText>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Auto-Revert After Animation
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
<SplitText
|
|
162
|
+
revertOnComplete
|
|
163
|
+
onSplit={({ chars }) => {
|
|
164
|
+
return animate(chars, { opacity: [0, 1] }, { delay: stagger(0.02) });
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<h1>Reverts to original HTML after animation</h1>
|
|
168
|
+
</SplitText>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Responsive Re-split
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<SplitText
|
|
175
|
+
autoSplit
|
|
176
|
+
onSplit={({ lines }) => animateLines(lines)}
|
|
177
|
+
onResize={({ lines }) => animateLines(lines)}
|
|
178
|
+
>
|
|
179
|
+
<p>Re-animates when container resizes</p>
|
|
180
|
+
</SplitText>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### CSS-Only Animation with Index Props
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<SplitText options={{ type: 'chars', propIndex: true }}>
|
|
187
|
+
<h1 className="stagger-fade">Hello</h1>
|
|
188
|
+
</SplitText>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```css
|
|
192
|
+
.stagger-fade .split-char {
|
|
193
|
+
opacity: 0;
|
|
194
|
+
animation: fade-in 0.5s forwards;
|
|
195
|
+
animation-delay: calc(var(--char-index) * 0.03s);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes fade-in {
|
|
199
|
+
to { opacity: 1; }
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### With GSAP
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { SplitText } from 'fetta/react';
|
|
207
|
+
import gsap from 'gsap';
|
|
208
|
+
|
|
209
|
+
<SplitText
|
|
210
|
+
revertOnComplete
|
|
211
|
+
onSplit={({ words }) => {
|
|
212
|
+
return gsap.from(words, {
|
|
213
|
+
opacity: 0,
|
|
214
|
+
y: 20,
|
|
215
|
+
stagger: 0.05,
|
|
216
|
+
duration: 0.6,
|
|
217
|
+
});
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<h1>Works with GSAP</h1>
|
|
221
|
+
</SplitText>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Nested HTML Elements
|
|
225
|
+
|
|
226
|
+
Fetta preserves inline elements like links, emphasis, and other formatting. Attributes (href, class, id, data-*, etc.) are maintained.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<SplitText
|
|
230
|
+
onSplit={({ chars }) => {
|
|
231
|
+
animate(chars, { opacity: [0, 1] }, { delay: stagger(0.02) });
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<p>Click <a href="/signup">here</a> to <em>get started</em></p>
|
|
235
|
+
</SplitText>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## CSS Classes
|
|
239
|
+
|
|
240
|
+
Default classes applied to split elements:
|
|
241
|
+
|
|
242
|
+
| Class | Element | Notes |
|
|
243
|
+
|-------|---------|-------|
|
|
244
|
+
| `.split-char` | Characters | Inline positioning |
|
|
245
|
+
| `.split-word` | Words | Inline positioning |
|
|
246
|
+
| `.split-line` | Lines | Block display |
|
|
247
|
+
|
|
248
|
+
Each element also receives a `data-index` attribute with its position.
|
|
249
|
+
|
|
250
|
+
## Notes
|
|
251
|
+
|
|
252
|
+
- **Fonts must be loaded** before splitting. The React component waits for `document.fonts.ready` automatically.
|
|
253
|
+
- **Ligatures are disabled** (`font-variant-ligatures: none`) because ligatures cannot span multiple elements.
|
|
254
|
+
- **Accessibility**: Split elements receive an `aria-label` with the original text for screen readers.
|
|
255
|
+
|
|
256
|
+
## Browser Support
|
|
257
|
+
|
|
258
|
+
All modern browsers: Chrome, Firefox, Safari, Edge.
|
|
259
|
+
|
|
260
|
+
Requires:
|
|
261
|
+
- `ResizeObserver`
|
|
262
|
+
- `IntersectionObserver`
|
|
263
|
+
- `Intl.Segmenter`
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT
|