ntly, grapheme-safe splitting ensures that typography motion degrades gracefully across global audiences instead of failing silently on non-ASCII inputs.
Core Solution
Building a production-ready typography motion system requires four architectural decisions: preset-as-data serialization, grapheme-aware splitting, single-master-clock seeking, and framework-agnostic initialization. The following implementation demonstrates how to construct this pipeline using @vysmo/text as the runtime foundation.
Step 1: Initialize with Grapheme-Safe Splitting
Traditional string iteration breaks Unicode clusters. The correct approach delegates boundary detection to the browser's native segmentation API.
import { initTypographyMotion } from "@vysmo/text";
const headline = document.querySelector("#hero-title") as HTMLElement;
const motionHandle = initTypographyMotion(headline, {
choreography: "arrive/slide-elevate",
splitMode: "character",
autoPlay: true,
});
The runtime automatically invokes Intl.Segmenter to slice the text into valid grapheme clusters. This ensures that emoji families, regional flag sequences, and combining diacritics remain intact as single DOM units. The splitMode parameter supports "character", "word", and "line", all routed through the same segmentation pipeline.
Step 2: Understand Preset Architecture
Presets are not magic strings. They are plain data objects that define transition parameters. This design enables tree-shaking, runtime inspection, and safe mutation.
import { slideElevate } from "@vysmo/text";
console.log(slideElevate);
/*
{
identifier: "arrive/slide-elevate",
split: "character",
staggerDelay: 35,
transitions: [
{ property: "opacity", start: 0, end: 1, duration: 480, easing: "power2.out" },
{ property: "translateY", start: 18, end: 0, duration: 480, easing: "power2.out" }
]
}
*/
When you pass the preset by reference, bundlers eliminate unused exports. The string-key variant ("arrive/slide-elevate") loads a lightweight registry for developer ergonomics. For production builds targeting two or three motion styles, reference passing keeps the payload under 2 KB.
Step 3: Calibrate Stagger and Direction
Stagger controls the temporal offset between consecutive grapheme units. Direction controls the initiation sequence. These two parameters alone can transform a preset's perceived rhythm without altering its core transitions.
// Deliberate, editorial pacing
initTypographyMotion(subtitle, {
choreography: "arrive/slide-elevate",
staggerDelay: 75,
staggerDirection: "center",
});
// Urgent, data-driven reveal
initTypographyMotion(metricValue, {
choreography: "arrive/slide-elevate",
staggerDelay: 12,
staggerDirection: "start",
});
staggerDirection accepts "start", "end", "center", "edges", and "random". The "center" variant is particularly effective for hero typography, as it creates a radial bloom effect that mimics projection rather than typing. Most timeline libraries do not expose directional staggering, forcing developers to manually calculate indices.
The motion handle exposes a .seek(progress) method that maps a 0β1 value to the animation timeline. This abstraction relies on a single-master-clock architecture, ensuring all grapheme units share the same temporal origin.
const scrollMotion = initTypographyMotion(narrativeBlock, {
choreography: "arrive/dissolve-forward",
autoPlay: false,
});
function syncToScroll(scrollProgress: number) {
const clamped = Math.min(1, Math.max(0, scrollProgress));
scrollMotion.seek(clamped);
}
// Attach to IntersectionObserver or scroll container
window.addEventListener("scroll", () => {
const rect = narrativeBlock.getBoundingClientRect();
const progress = 1 - (rect.top / window.innerHeight);
requestAnimationFrame(() => syncToScroll(progress));
});
This pattern eliminates manual interpolation, easing recalculation, and RAF loop management. The single-master-clock ensures that seeking remains coherent even when the user scrolls rapidly or reverses direction.
Step 5: React Integration
For declarative frameworks, the companion package @vysmo/text-react wraps the runtime in a component and hook.
import { TextMotionProvider, useTypographyMotion } from "@vysmo/text-react";
export function DashboardMetric() {
const motionRef = useTypographyMotion({
choreography: "emphasis/pulse-soft",
trigger: "hover",
});
return (
<span ref={motionRef} className="metric-value">
1,247
</span>
);
}
The hook defers initialization to useEffect, preventing hydration mismatches. Props pass directly to the underlying runtime, maintaining parity between vanilla and framework implementations.
Pitfall Guide
1. Naive String Splitting
Explanation: Using [...text] or .split('') breaks Unicode grapheme clusters. Emoji families, ZWJ sequences, and combining marks fragment into broken DOM nodes, causing visual glitches and accessibility failures.
Fix: Always rely on Intl.Segmenter or a library that implements it by default. Verify splitting behavior with test strings containing regional flags, skin-tone modifiers, and CJK characters.
2. Stagger Overload
Explanation: Setting staggerDelay too low (<10ms) creates a visual wash that negates choreography. Setting it too high (>100ms) disrupts reading rhythm and increases perceived load time.
Fix: Calibrate stagger to content density. Use 30β50ms for headlines, 15β25ms for short labels, and 60β80ms for editorial paragraphs. Test on mid-tier devices to ensure smooth frame pacing.
3. Ignoring Exit Choreographies
Explanation: Developers focus exclusively on entrance animations, leaving text to disappear abruptly or rely on CSS display: none. This breaks spatial continuity and hurts perceived performance.
Fix: Pair every entrance preset with a corresponding exit preset. Use "exit/fade-descend" or "exit/collapse-burst" for state transitions. Ensure exit durations are slightly shorter than entrances to maintain forward momentum.
Explanation: Attaching .seek() directly to scroll events without throttling or RAF batching causes layout thrashing and dropped frames, especially on mobile GPUs.
Fix: Wrap .seek() in requestAnimationFrame. Debounce or throttle scroll listeners. Prefer IntersectionObserver for viewport-triggered animations instead of continuous scroll polling.
5. React Hydration Mismatches
Explanation: Initializing typography motion during render causes server/client HTML mismatches, triggering React warnings and layout shifts.
Fix: Defer all motion initialization to useEffect or use framework wrappers that handle SSR safely. Avoid inline style injection during the render phase.
6. Preset Mutation
Explanation: Directly modifying a preset object (preset.stagger = 50) mutates shared references, causing unpredictable behavior across multiple instances.
Fix: Treat presets as immutable. Clone before modification: const customPreset = { ...basePreset, staggerDelay: 50 }. Use runtime override parameters instead of mutating the source object.
7. Over-Engineering Emphasis Loops
Explanation: Building custom setInterval or requestAnimationFrame loops for pulse/shake effects duplicates library functionality and introduces memory leaks.
Fix: Use dedicated emphasis presets ("emphasis/pulse", "emphasis/shake", "emphasis/wobble"). Configure repeat and duration via runtime parameters. Let the library manage the animation loop lifecycle.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Landing page hero | Preset reference + "center" stagger | Delivers editorial polish with minimal bundle overhead | ~1.5 KB gzipped |
| Dashboard metric badge | Emphasis preset + hover trigger | Lightweight, loop-safe, no scroll dependency | ~0.8 KB gzipped |
| Scroll-driven narrative | .seek() + IntersectionObserver | Coherent timeline sync, no manual interpolation | ~2.0 KB gzipped |
| Multi-language app | Grapheme-safe split + RTL support | Prevents cluster fragmentation across global inputs | ~2.5 KB gzipped |
| Legacy browser fallback | CSS keyframes + JS toggle | Graceful degradation when Intl.Segmenter unavailable | 0 KB (native) |
Configuration Template
// motion.config.ts
import { defineTypographyMotion } from "@vysmo/text";
export const editorialPreset = defineTypographyMotion({
identifier: "custom/editorial-reveal",
split: "character",
staggerDelay: 45,
staggerDirection: "center",
transitions: [
{ property: "opacity", start: 0, end: 1, duration: 520, easing: "power3.out" },
{ property: "translateY", start: 24, end: 0, duration: 520, easing: "power3.out" },
{ property: "filter", start: "blur(4px)", end: "blur(0px)", duration: 400, easing: "ease.out" }
]
});
export const dataPreset = defineTypographyMotion({
identifier: "custom/data-snap",
split: "character",
staggerDelay: 15,
staggerDirection: "start",
transitions: [
{ property: "opacity", start: 0, end: 1, duration: 300, easing: "linear" },
{ property: "scale", start: 0.85, end: 1, duration: 280, easing: "back.out(1.2)" }
]
});
Quick Start Guide
- Install the runtime:
npm install @vysmo/text
- Import the initializer and a preset:
import { initTypographyMotion, slideElevate } from "@vysmo/text"
- Select your target element:
const target = document.querySelector("#headline")
- Initialize with reference passing:
initTypographyMotion(target, { choreography: slideElevate })
- Verify grapheme safety by testing with emoji and non-Latin scripts before shipping.