Motion (React) — Beta
Motion (React)BETA
Variant-driven SplitText built on Motion. It uses the same variants, initial, animate, exit, and whileInView model, with extra support for per-type targets, split-aware function variants, and scroll progress.
Includes all props from the React component (callbacks, viewport, initialStyles, etc.), plus the motion specific props below. Requires motion to be installed.
Shared React Props
fetta/motion includes every prop from fetta/react:
Callback props can return animations/promises:
type CallbackReturn =
| void
| { finished: Promise<unknown> }
| Array<{ finished: Promise<unknown> }>
| Promise<unknown>;Motion Props
ScrollPropOptions
Wrapper Element
SplitText renders a <motion.div> wrapper (configurable via as) around your child. The wrapper:
- Forwards standard Motion/DOM wrapper props (
id,role,tabIndex,layout,drag,data-*, etc.) plus anywrappervariant targets. - Starts with
visibility: hiddenwhile fonts/split output are prepared, then switches tovisible. WithwaitForFonts={false}, it can render immediately. - Has
position: relativeapplied by default. - Handles orchestration keys (
staggerChildren,delayChildren,when) from thetransitionprop.
Quick Start
Pass targets directly to initial and animate when you do not need named variants:
Use named variants for reusable states or triggers like whileInView and whileHover:
Variant Definitions
Fetta supports three variant shapes:
Flat Variants
Standard Motion target objects. Applied to the smallest split type in options.type.
Target resolution: Flat variants always target the smallest type available:
chars→words→lines. If you split intochars,words, a flat variant targetschars. To target a larger type, use per-type keys or restrictoptions.type.
Per-Type Variants
Target different element types in one variant. Each type can define its own transition. A top-level transition acts as the default unless overridden.
Per-type variants can include a wrapper key to animate the outer wrapper element alongside split nodes. Wrapper values can be objects or functions that receive { custom }:
Function Variants
Any per-type value (or flat variant) can be a function that receives VariantInfo and returns a target. This enables position-aware animation like per-line stagger:
VariantInfo
Function variants receive VariantInfo, which describes each element's position in the split:
interface VariantInfo<TCustom = unknown> {
index: number; // position within nearest split parent
count: number; // total elements in that parent group
globalIndex: number; // absolute position across all elements
globalCount: number; // total elements of this type
lineIndex: number; // parent line index (0 if lines not split)
wordIndex: number; // parent word index (0 if words not split)
isPresent: boolean; // AnimatePresence presence state
custom?: TCustom; // custom data from the SplitText prop
}Grouping rules: index/count reset per parent group — chars reset per line (or per word if lines aren't split), words reset per line. globalIndex/globalCount are always continuous.
For example, splitting "Hello World" into chars,words:
Because index resets per word, delayScope="local" restarts stagger timing per word (0–4). Use globalIndex or delayScope="global" (default) for a continuous stagger.
Inline Variants
initial, animate, and exit accept full variant definitions (objects/functions), not only names. They use the same target resolution and per-type rules as named variants:
whileScroll also supports inline variant definitions:
The same inline syntax works for all split-trigger while* props (whileInView, whileOutOfView, whileHover, whileTap, whileFocus):
You can also pass function variants directly to triggers:
Per-type function variants also work inside inline trigger objects:
Transitions
Transition precedence (highest to lowest):
transitionreturned by a function varianttransitioninside a per-type targettransitionprop onSplitText
Orchestration keys (staggerChildren, delayChildren, when) apply only to the wrapper. Per-element transitions are set on split nodes.
The delay field accepts stagger() return values. The stagger function receives (index, count) based on delayScope.
delayScope
Controls how stagger() counts elements:
"global"(default) — one continuous stagger across all elements"local"— stagger restarts within each parent group
Trigger Priority
When multiple triggers are active, the highest-priority trigger wins:
Scroll takes full control:
whileScroll— scroll position drives animation progress, all other triggers ignored
Interactions temporarily override the active state: 2. whileTap 3. whileFocus 4. whileHover
Viewport and base determine the resting state: 5. whileInView 6. whileOutOfView 7. animate
Interaction triggers return to the current base state on release and activate only when variants are defined.
Viewport
Uses the same viewport options as the React component.
resetOnViewportLeave instantly re-applies the initial variant when the element leaves the viewport. Ignored when viewport.once is true.
Avoid combining
whileOutOfViewwithresetOnViewportLeave— the instant reset will override the leave animation.
Exit (AnimatePresence)
This follows standard Motion exit behavior. SplitText must be a direct child of AnimatePresence.
Important: Exit completion is tracked across both split nodes (
chars/words/lines) and the wrapper variant target. Ifexitis unset or no tracked target matches, unmount proceeds immediately.
VariantInfo.isPresent is false during exit, letting function variants adjust behavior.
Revert on Complete
revertOnComplete restores original HTML after animate finishes on all split nodes. It works only with animate (no viewport/scroll triggers).
If the variant does not target split nodes, revert happens immediately. Callback return values do not control this; variant completion does.
Use onRevert to observe when revert completes:
Callbacks
onSplit, onViewportEnter, and onViewportLeave still fire with SplitTextElements. Use them for side effects; they do not override declarative animation.
onSplit runs once for the active split cycle. It is not replayed during autoSplit resplits.
onResplit runs every time a full resplit replaces split output nodes.
animateOnResplit controls whether declarative initial/animate variants replay during those full resplits (default: false).
In callback mode with autoSplit + lines, reattach runtime wiring (like scroll(...) subscriptions) in onResplit.
In variant mode (whileScroll, whileInView, etc.), rebinding after internal full resplits is handled automatically.
onRevert is also available as a zero-argument callback when a split cycle reverts.
For practical usage patterns, see the Motion examples.