fetta 1.4.4 → 1.5.1
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 +272 -29
- package/dist/chunk-FP4I6OR2.js +43 -0
- package/dist/chunk-ORMEWXMH.js +33 -0
- package/dist/chunk-UPF3IYHC.js +1762 -0
- package/dist/helpers.d.ts +46 -0
- package/dist/helpers.js +121 -0
- package/dist/index-c1UKfWWK.d.ts +144 -0
- package/dist/index.d.ts +1 -137
- package/dist/index.js +2 -1
- package/dist/initialStyles-BGuPp5CS.d.ts +23 -0
- package/dist/motion.d.ts +185 -0
- package/dist/motion.js +2116 -0
- package/dist/react.d.ts +37 -76
- package/dist/react.js +136 -101
- package/package.json +26 -2
- package/dist/chunk-Y4GCLM4K.js +0 -1077
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fetta
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Text splitting that keeps kerning intact.
|
|
4
4
|
|
|
5
5
|
Split text into characters, words, and lines while preserving the original typography. Works with any animation library.
|
|
6
6
|
|
|
@@ -8,17 +8,17 @@ Split text into characters, words, and lines while preserving the original typog
|
|
|
8
8
|
|
|
9
9
|
- **Kerning Compensation** — Maintains original character spacing when splitting by chars
|
|
10
10
|
- **Nested Elements** — Preserves `<a>`, `<em>`, `<strong>` and other inline elements with all attributes
|
|
11
|
-
- **Line Detection** —
|
|
11
|
+
- **Line Detection** — Groups words into rendered lines
|
|
12
12
|
- **Dash Handling** — Allows text to wrap naturally after em-dashes, en-dashes, hyphens, and slashes
|
|
13
13
|
- **Auto Re-split** — Re-splits on container resize
|
|
14
|
-
- **Auto
|
|
14
|
+
- **Auto Revert** — Restores original HTML after animations
|
|
15
15
|
- **Masking** — Wrap elements in clip containers for reveal animations
|
|
16
16
|
- **Emoji Support** — Properly handles compound emojis and complex Unicode characters
|
|
17
17
|
- **Accessible** — Automatic screen reader support, even when splitting text with nested links or emphasis
|
|
18
18
|
- **TypeScript** — Full type definitions included
|
|
19
19
|
- **React Component** — Declarative wrapper for React projects
|
|
20
|
-
- **
|
|
21
|
-
- **Library Agnostic** — Works with Motion, GSAP, or any animation library
|
|
20
|
+
- **Viewport Triggers** — Scroll enter/leave callbacks with configurable thresholds in React
|
|
21
|
+
- **Library Agnostic** — Works with Motion, GSAP, CSS, or any animation library
|
|
22
22
|
|
|
23
23
|
## Installation
|
|
24
24
|
|
|
@@ -26,7 +26,11 @@ Split text into characters, words, and lines while preserving the original typog
|
|
|
26
26
|
npm install fetta
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
**Bundle size
|
|
29
|
+
**Bundle size** (minified + brotli)
|
|
30
|
+
- `fetta`: ~7.17 kB
|
|
31
|
+
- `fetta/react`: ~8.66 kB
|
|
32
|
+
- `fetta/motion`: ~15.93 kB
|
|
33
|
+
- `fetta/helpers`: ~765 B
|
|
30
34
|
|
|
31
35
|
## Quick Start
|
|
32
36
|
|
|
@@ -83,13 +87,13 @@ const result = splitText(element, options);
|
|
|
83
87
|
| `lineClass` | `string` | `"split-line"` | CSS class for line elements |
|
|
84
88
|
| `mask` | `string` | — | Wrap elements in `overflow: clip` container: `"chars"`, `"words"`, or `"lines"` |
|
|
85
89
|
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
86
|
-
| `
|
|
87
|
-
| `onSplit` | `function` | — | Callback after initial split |
|
|
90
|
+
| `onResplit` | `function` | — | Callback after autoSplit/full-resplit replaces split output elements |
|
|
91
|
+
| `onSplit` | `function` | — | Callback after initial split. Return animation/promise for `revertOnComplete` |
|
|
88
92
|
| `revertOnComplete` | `boolean` | `false` | Auto-revert when animation completes |
|
|
89
93
|
| `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
|
|
90
94
|
| `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
|
|
91
95
|
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split. Values can be objects or `(el, index) => object` functions |
|
|
92
|
-
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines
|
|
96
|
+
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values are strings |
|
|
93
97
|
|
|
94
98
|
#### Return Value
|
|
95
99
|
|
|
@@ -102,13 +106,57 @@ const result = splitText(element, options);
|
|
|
102
106
|
}
|
|
103
107
|
```
|
|
104
108
|
|
|
109
|
+
### `createSplitClones(splitResult, options)` (`fetta/helpers`)
|
|
110
|
+
|
|
111
|
+
Builds swap/reveal DOM layers (clones + optional wrappers) without coupling to any animation library.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { splitText } from "fetta";
|
|
115
|
+
import { createSplitClones } from "fetta/helpers";
|
|
116
|
+
|
|
117
|
+
const split = splitText(element, { type: "chars", mask: "chars" });
|
|
118
|
+
const layers = createSplitClones(split, { unit: "chars", wrap: true });
|
|
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()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Behavior
|
|
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
|
+
|
|
105
149
|
### `<SplitText>` (React)
|
|
106
150
|
|
|
107
151
|
```tsx
|
|
108
152
|
import { SplitText } from 'fetta/react';
|
|
109
153
|
```
|
|
110
154
|
|
|
111
|
-
|
|
155
|
+
`fetta/react` forwards common wrapper DOM props (`id`, `role`, `tabIndex`, `aria-*`, `data-*`, and event handlers like `onClick`) to the wrapper element.
|
|
156
|
+
|
|
157
|
+
`fetta/react` props:
|
|
158
|
+
|
|
159
|
+
#### React Props
|
|
112
160
|
|
|
113
161
|
| Prop | Type | Default | Description |
|
|
114
162
|
|------|------|---------|-------------|
|
|
@@ -117,21 +165,35 @@ import { SplitText } from 'fetta/react';
|
|
|
117
165
|
| `className` | `string` | — | Class name for wrapper element |
|
|
118
166
|
| `style` | `CSSProperties` | — | Additional styles for wrapper element |
|
|
119
167
|
| `ref` | `Ref<HTMLElement>` | — | Ref to container element |
|
|
120
|
-
| `onSplit` | `(result) =>
|
|
121
|
-
| `
|
|
122
|
-
| `options` | `
|
|
168
|
+
| `onSplit` | `(result) => CallbackReturn` | — | Called after text is split |
|
|
169
|
+
| `onResplit` | `(result) => void` | — | Called when autoSplit/full-resplit replaces split output elements |
|
|
170
|
+
| `options` | `SplitTextOptions` | — | Split options (type, classes, mask, propIndex, disableKerning) |
|
|
123
171
|
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
172
|
+
| `waitForFonts` | `boolean` | `true` | Wait for `document.fonts.ready` before splitting (recommended for stable kerning). Set `false` for immediate split. |
|
|
124
173
|
| `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
174
|
+
| `onRevert` | `() => void` | — | Called when split text is reverted (manual or automatic) |
|
|
175
|
+
| `viewport` | `ViewportOptions` | — | Configure viewport detection |
|
|
176
|
+
| `onViewportEnter` | `(result) => CallbackReturn` | — | Called when element enters viewport |
|
|
177
|
+
| `onViewportLeave` | `(result) => CallbackReturn` | — | Called when element leaves viewport |
|
|
128
178
|
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines. Values can be objects or `(el, index) => object` functions |
|
|
129
|
-
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values
|
|
130
|
-
| `
|
|
179
|
+
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values are strings |
|
|
180
|
+
| `resetOnViewportLeave` | `boolean` | `false` | Re-apply initialStyles/initialClasses when leaving viewport |
|
|
181
|
+
|
|
182
|
+
#### Shared `SplitTextOptions` (`options` prop)
|
|
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) |
|
|
131
193
|
|
|
132
194
|
#### Callback Signature
|
|
133
195
|
|
|
134
|
-
All callbacks (`onSplit`, `
|
|
196
|
+
All callbacks (`onSplit`, `onResplit`, `onViewportEnter`, `onViewportLeave`) receive:
|
|
135
197
|
|
|
136
198
|
```ts
|
|
137
199
|
{
|
|
@@ -142,16 +204,85 @@ All callbacks (`onSplit`, `onResize`, `onInView`, `onLeaveView`) receive the sam
|
|
|
142
204
|
}
|
|
143
205
|
```
|
|
144
206
|
|
|
145
|
-
|
|
207
|
+
```ts
|
|
208
|
+
type CallbackReturn =
|
|
209
|
+
| void
|
|
210
|
+
| Promise<unknown>
|
|
211
|
+
| { finished: Promise<unknown> }
|
|
212
|
+
| { then: (onFulfilled?: ((result: unknown) => unknown) | undefined) => unknown }
|
|
213
|
+
| CallbackReturn[];
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
When using `autoSplit` with `lines` in scroll-linked or scroll-triggered animations, re-attach scroll/timeline logic inside `onResplit` so it binds to the new split element references.
|
|
217
|
+
|
|
218
|
+
`onRevert` is a separate zero-argument callback that fires when a split cycle actually reverts.
|
|
219
|
+
|
|
220
|
+
#### Viewport Options
|
|
146
221
|
|
|
147
222
|
```ts
|
|
148
223
|
{
|
|
149
|
-
amount?: number
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
229
|
}
|
|
153
230
|
```
|
|
154
231
|
|
|
232
|
+
### `<SplitText>` (Motion)
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { SplitText } from "fetta/motion";
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`fetta/motion` includes all props from `fetta/react`, plus Motion variant props. It also forwards standard Motion/DOM wrapper props (`id`, `role`, `tabIndex`, `layout`, `drag`, `data-*`, etc.) to the wrapper.
|
|
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
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Motion-only Props
|
|
264
|
+
|
|
265
|
+
| Prop | Type | Default | Description |
|
|
266
|
+
|------|------|---------|-------------|
|
|
267
|
+
| `variants` | `Record<string, VariantDefinition<TCustom>>` | — | Named variant definitions |
|
|
268
|
+
| `initial` | `string \| VariantDefinition<TCustom> \| false` | — | Initial variant applied instantly after split |
|
|
269
|
+
| `animate` | `string \| VariantDefinition<TCustom>` | — | Base variant |
|
|
270
|
+
| `exit` | `string \| VariantDefinition<TCustom> \| false` | — | Exit variant (AnimatePresence) |
|
|
271
|
+
| `whileInView` | `string \| VariantDefinition<TCustom>` | — | Variant while element is in view |
|
|
272
|
+
| `whileOutOfView` | `string \| VariantDefinition<TCustom>` | — | Variant after element leaves view |
|
|
273
|
+
| `whileScroll` | `string \| VariantDefinition<TCustom>` | — | Scroll-driven variant (highest trigger priority) |
|
|
274
|
+
| `whileHover` | `string \| VariantDefinition<TCustom>` | — | Variant on hover |
|
|
275
|
+
| `whileTap` | `string \| VariantDefinition<TCustom>` | — | Variant on tap/press |
|
|
276
|
+
| `whileFocus` | `string \| VariantDefinition<TCustom>` | — | Variant on focus |
|
|
277
|
+
| `animateOnResplit` | `boolean` | `false` | Replay initial->animate on autoSplit/full-resplit |
|
|
278
|
+
| `scroll` | `{ offset?, axis?, container? }` | — | Scroll tracking options for `whileScroll` |
|
|
279
|
+
| `transition` | `AnimationOptions` | — | Global/default transition for variants |
|
|
280
|
+
| `custom` | `TCustom` | — | Custom data forwarded to function variants |
|
|
281
|
+
| `delayScope` | `"global" \| "local"` | `"global"` | Delay-function index scope (`globalIndex` vs relative `index`) |
|
|
282
|
+
| `reducedMotion` | `"user" \| "always" \| "never"` | — | Reduced-motion behavior for this component |
|
|
283
|
+
| `onHoverStart` | `() => void` | — | Called when hover starts |
|
|
284
|
+
| `onHoverEnd` | `() => void` | — | Called when hover ends |
|
|
285
|
+
|
|
155
286
|
## Examples
|
|
156
287
|
|
|
157
288
|
### Vanilla JavaScript
|
|
@@ -243,7 +374,7 @@ splitText(element, { type: 'chars', propIndex: true });
|
|
|
243
374
|
</SplitText>
|
|
244
375
|
```
|
|
245
376
|
|
|
246
|
-
#### Scroll-Triggered with
|
|
377
|
+
#### Scroll-Triggered with Viewport
|
|
247
378
|
|
|
248
379
|
```tsx
|
|
249
380
|
<SplitText
|
|
@@ -251,11 +382,11 @@ splitText(element, { type: 'chars', propIndex: true });
|
|
|
251
382
|
initialStyles={{
|
|
252
383
|
words: { opacity: '0', transform: 'translateY(20px)' }
|
|
253
384
|
}}
|
|
254
|
-
|
|
255
|
-
|
|
385
|
+
viewport={{ amount: 0.5 }}
|
|
386
|
+
onViewportEnter={({ words }) => {
|
|
256
387
|
animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
|
|
257
388
|
}}
|
|
258
|
-
|
|
389
|
+
resetOnViewportLeave
|
|
259
390
|
>
|
|
260
391
|
<p>Animates when scrolled into view</p>
|
|
261
392
|
</SplitText>
|
|
@@ -274,6 +405,104 @@ splitText(element, { type: 'chars', propIndex: true });
|
|
|
274
405
|
</SplitText>
|
|
275
406
|
```
|
|
276
407
|
|
|
408
|
+
### Motion (`fetta/motion`)
|
|
409
|
+
|
|
410
|
+
#### Basic Variants
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
import { SplitText } from 'fetta/motion';
|
|
414
|
+
import { stagger } from 'motion';
|
|
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';
|
|
435
|
+
|
|
436
|
+
<SplitText
|
|
437
|
+
delayScope="local"
|
|
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>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Scroll-Driven Reveal
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { SplitText } from 'fetta/motion';
|
|
465
|
+
|
|
466
|
+
<SplitText
|
|
467
|
+
initialStyles={{ chars: { opacity: 0.2 } }}
|
|
468
|
+
whileScroll={{
|
|
469
|
+
chars: ({ globalIndex }) => ({
|
|
470
|
+
opacity: 1,
|
|
471
|
+
transition: {
|
|
472
|
+
duration: 0.3,
|
|
473
|
+
at: globalIndex * 0.025,
|
|
474
|
+
ease: "linear",
|
|
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
|
+
```
|
|
484
|
+
|
|
485
|
+
#### Hover Interaction
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
import { SplitText } from 'fetta/motion';
|
|
489
|
+
import { stagger } from 'motion';
|
|
490
|
+
|
|
491
|
+
<SplitText
|
|
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
|
+
```
|
|
505
|
+
|
|
277
506
|
## CSS Classes
|
|
278
507
|
|
|
279
508
|
Default classes applied to split elements:
|
|
@@ -284,7 +513,10 @@ Default classes applied to split elements:
|
|
|
284
513
|
| `.split-word` | Words | Inline positioning |
|
|
285
514
|
| `.split-line` | Lines | Block display |
|
|
286
515
|
|
|
287
|
-
|
|
516
|
+
Split elements receive typed index attributes:
|
|
517
|
+
- Characters: `data-char-index`
|
|
518
|
+
- Words: `data-word-index`
|
|
519
|
+
- Lines: `data-line-index`
|
|
288
520
|
|
|
289
521
|
## Font Loading
|
|
290
522
|
|
|
@@ -297,7 +529,17 @@ document.fonts.ready.then(() => {
|
|
|
297
529
|
});
|
|
298
530
|
```
|
|
299
531
|
|
|
300
|
-
|
|
532
|
+
React and Motion components wait for fonts by default (`waitForFonts={true}`), which gives the most stable kerning.
|
|
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
|
+
```
|
|
301
543
|
|
|
302
544
|
## Accessibility
|
|
303
545
|
|
|
@@ -332,6 +574,7 @@ Pre-existing `aria-label` attributes are always preserved.
|
|
|
332
574
|
## Notes
|
|
333
575
|
|
|
334
576
|
- **Ligatures are disabled** (`font-variant-ligatures: none`) because ligatures cannot span multiple elements.
|
|
577
|
+
- **Authored hard breaks are preserved** — Explicit `<br>` and block-level descendants are treated as hard boundaries. In `chars`/`words` modes, hard boundaries are normalized to `<br>` in the split output.
|
|
335
578
|
|
|
336
579
|
## Browser Support
|
|
337
580
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/internal/initialStyles.ts
|
|
2
|
+
function reapplyInitialStyles(elements, style) {
|
|
3
|
+
if (!style || elements.length === 0) return;
|
|
4
|
+
const isFn = typeof style === "function";
|
|
5
|
+
for (let i = 0; i < elements.length; i++) {
|
|
6
|
+
const el = elements[i];
|
|
7
|
+
const styles = isFn ? style(el, i) : style;
|
|
8
|
+
for (const [key, value] of Object.entries(styles)) {
|
|
9
|
+
if (value == null) continue;
|
|
10
|
+
if (key === "cssText") {
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
el.style.cssText = value;
|
|
13
|
+
}
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value !== "string" && typeof value !== "number") continue;
|
|
17
|
+
const cssValue = typeof value === "number" ? String(value) : value;
|
|
18
|
+
const cssKey = key.startsWith("--") ? key : key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
19
|
+
el.style.setProperty(cssKey, cssValue);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function reapplyInitialClasses(elements, className) {
|
|
24
|
+
if (!className || elements.length === 0) return;
|
|
25
|
+
const classes = className.split(/\s+/).filter(Boolean);
|
|
26
|
+
for (const el of elements) {
|
|
27
|
+
el.classList.add(...classes);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/internal/waitForFontsReady.ts
|
|
32
|
+
async function waitForFontsReady(waitForFonts) {
|
|
33
|
+
if (!waitForFonts) return;
|
|
34
|
+
const fonts = document.fonts;
|
|
35
|
+
const ready = fonts == null ? void 0 : fonts.ready;
|
|
36
|
+
if (!ready || typeof ready.then !== "function") return;
|
|
37
|
+
try {
|
|
38
|
+
await ready;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { reapplyInitialClasses, reapplyInitialStyles, waitForFontsReady };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
var __objRest = (source, exclude) => {
|
|
21
|
+
var target = {};
|
|
22
|
+
for (var prop in source)
|
|
23
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
24
|
+
target[prop] = source[prop];
|
|
25
|
+
if (source != null && __getOwnPropSymbols)
|
|
26
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
27
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
28
|
+
target[prop] = source[prop];
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { __objRest, __spreadProps, __spreadValues };
|