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 any wrapper variant targets.
  • Starts with visibility: hidden while fonts/split output are prepared, then switches to visible. With waitForFonts={false}, it can render immediately.
  • Has position: relative applied by default.
  • Handles orchestration keys (staggerChildren, delayChildren, when) from the transition prop.

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: charswordslines. If you split into chars,words, a flat variant targets chars. To target a larger type, use per-type keys or restrict options.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):

  1. transition returned by a function variant
  2. transition inside a per-type target
  3. transition prop on SplitText

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:

  1. 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 whileOutOfView with resetOnViewportLeave — 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. If exit is 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.