Skip to main content

Overview

The Motion <SplitText> component combines Griffo’s text splitting with Motion’s declarative animation system. Use variants, whileInView, whileHover, and all Motion animation props.
import { SplitText } from "griffo/motion";
Bundle size: 13.78 kB (minified + brotli)

Basic Usage

import { SplitText } from "griffo/motion";
import { stagger } from "motion";

<SplitText
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.65, delay: stagger(0.04) }}
  options={{ type: "words" }}
>
  <h1>Hello World</h1>
</SplitText>;

Per-Type Targets

Animate different split types independently:
<SplitText
  initial={{
    chars: { opacity: 0, y: 10 },
    words: { scale: 0.9 },
  }}
  animate={{
    chars: { opacity: 1, y: 0 },
    words: { scale: 1 },
  }}
  options={{ type: "chars,words" }}
>
  <h1>Different animations per type</h1>
</SplitText>

Supported Keys

initial={{ chars: { opacity: 0 } }}
animate={{ chars: { opacity: 1 } }}

Function Variants

Access element index, line/word context, and custom data:
<SplitText
  initial={{
    chars: ({ index, count, lineIndex }) => ({
      opacity: 0,
      y: 10,
      transition: {
        delay: lineIndex * 0.15 + (index / count) * 0.3,
      },
    }),
  }}
  animate={{ chars: { opacity: 1, y: 0 } }}
  options={{ type: "chars,lines" }}
>
  <p>Per-line staggered reveal</p>
</SplitText>

VariantInfo

index
number
Relative index within nearest parent (line > word > global)
count
number
Total elements in that parent group
globalIndex
number
Absolute index across all elements of this type
globalCount
number
Total elements of this type across entire split
lineIndex
number
Parent line index (0 if lines not split)
wordIndex
number
Parent word index (0 if words not split)
custom
TCustom | undefined
User custom data passed to <SplitText>

Named Variants

Define reusable variant states:
<SplitText
  variants={{
    hidden: { chars: { opacity: 0, y: 10 } },
    visible: {
      chars: ({ lineIndex }) => ({
        opacity: 1,
        y: 0,
        transition: {
          delay: stagger(0.02, { startDelay: lineIndex * 0.15 }),
        },
      }),
    },
  }}
  initial="hidden"
  animate="visible"
  options={{ type: "chars,lines", mask: "lines" }}
>
  <p>Per-line staggered reveal</p>
</SplitText>

whileInView

Animate when scrolled into view:
<SplitText
  initial={{ opacity: 0, y: 20 }}
  whileInView={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.65, delay: stagger(0.05) }}
  viewport={{ amount: 0.5, once: true }}
  options={{ type: "words" }}
>
  <h2>Scroll-triggered animation</h2>
</SplitText>

Viewport Options

<SplitText
  whileInView={{ opacity: 1 }}
  viewport={{
    once: true, // Only trigger once
    amount: 0.5, // 50% visibility required
    margin: "0px 0px -100px 0px", // IntersectionObserver rootMargin
  }}
  options={{ type: "chars" }}
>
  <p>Trigger once at 50%</p>
</SplitText>

whileHover

Animate on hover:
<SplitText
  animate={{ chars: { opacity: 1, y: 0 } }}
  whileHover={{
    chars: ({ index, count }) => ({
      y: -5,
      transition: {
        delay: (index / count) * 0.1,
      },
    }),
  }}
  options={{ type: "chars" }}
>
  <h3>Hover to bounce</h3>
</SplitText>

Hover Callbacks

<SplitText
  whileHover={{ chars: { scale: 1.2 } }}
  onHoverStart={() => console.log("Hover started")}
  onHoverEnd={() => console.log("Hover ended")}
  options={{ type: "chars" }}
>
  <button>Hover me</button>
</SplitText>

whileTap & whileFocus

<SplitText
  animate={{ chars: { opacity: 1 } }}
  whileTap={{ chars: { scale: 0.95 } }}
  whileFocus={{ chars: { color: "#ff0080" } }}
  options={{ type: "chars" }}
>
  <button>Click or focus me</button>
</SplitText>

whileScroll

Drive animation progress with scroll position:
<SplitText
  whileScroll={{
    chars: ({ index, count }) => ({
      opacity: [0, 1, 1, 0],
      scale: [0.8, 1, 1, 0.8],
    }),
  }}
  scroll={{
    offset: ["start end", "end start"],
  }}
  options={{ type: "chars" }}
>
  <h1>Parallax text</h1>
</SplitText>

Scroll Options

scroll.offset
MotionScrollOffset
default:["start end","end start"]
Scroll range for animation progress
scroll.axis
'x' | 'y'
default:"y"
Scroll axis to track
scroll.container
React.RefObject<Element>
Custom scroll container (default: nearest scrollable ancestor)

Custom Data

Pass custom data to function variants:
<SplitText
  custom={{ color: "#ff0080", duration: 0.8 }}
  initial={{
    chars: ({ custom }) => ({
      opacity: 0,
      color: custom?.color,
    }),
  }}
  animate={{
    chars: ({ custom }) => ({
      opacity: 1,
      transition: { duration: custom?.duration },
    }),
  }}
  options={{ type: "chars" }}
>
  <h1>Custom variant data</h1>
</SplitText>

Delay Scope

Control how delay functions resolve indices:
// Global scope (default)
<SplitText
  delayScope="global"
  initial={{
    chars: ({ globalIndex, globalCount }) => ({
      opacity: 0,
      transition: { delay: globalIndex * 0.02 },
    }),
  }}
  animate={{ chars: { opacity: 1 } }}
  options={{ type: "chars,lines" }}
>
  <p>Delays based on global char index</p>
</SplitText>

// Local scope (per-line)
<SplitText
  delayScope="local"
  initial={{
    chars: ({ index, count }) => ({
      opacity: 0,
      transition: { delay: index * 0.02 }, // Resets per line
    }),
  }}
  animate={{ chars: { opacity: 1 } }}
  options={{ type: "chars,lines" }}
>
  <p>Delays reset per line</p>
</SplitText>

AnimatePresence Exit

import { AnimatePresence } from "motion/react";
import { SplitText } from "griffo/motion";

function App() {
  const [show, setShow] = useState(true);

  return (
    <AnimatePresence>
      {show && (
        <SplitText
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{
            chars: ({ index, count }) => ({
              opacity: 0,
              y: -20,
              transition: {
                delay: (index / count) * 0.05,
              },
            }),
          }}
          options={{ type: "chars" }}
        >
          <h1>Goodbye</h1>
        </SplitText>
      )}
    </AnimatePresence>
  );
}

Masking for Reveals

<SplitText
  initial={{ chars: { y: "100%" } }}
  animate={{ chars: { y: "0%" } }}
  transition={{ duration: 0.65, delay: stagger(0.03) }}
  options={{ type: "chars", mask: "chars" }}
>
  <h1>Slide up reveal</h1>
</SplitText>
mask: "chars" wraps each character in an overflow container for clean slide-in/out effects.

Auto-resplit on Resize

<SplitText
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  options={{ type: "words,lines" }}
  autoSplit
  animateOnResplit // Re-run initial->animate on resize
>
  <p>Responsive animation</p>
</SplitText>

Reduced Motion

<SplitText
  reducedMotion="user" // Respects prefers-reduced-motion
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  options={{ type: "words" }}
>
  <h1>Accessible animation</h1>
</SplitText>
reducedMotion
'user' | 'always' | 'never'
default:"user"
  • "user": Disable animations if prefers-reduced-motion is set
  • "always": Always disable animations
  • "never": Always enable animations

Complete Example

import { SplitText } from "griffo/motion";
import { stagger } from "motion";

export default function Hero() {
  return (
    <SplitText
      variants={{
        hidden: {
          chars: { opacity: 0, y: 20 },
          wrapper: { opacity: 0 },
        },
        visible: {
          wrapper: { opacity: 1 },
          chars: ({ lineIndex }) => ({
            opacity: 1,
            y: 0,
            transition: {
              delay: stagger(0.03, { startDelay: lineIndex * 0.15 }),
              duration: 0.65,
            },
          }),
        },
      }}
      initial="hidden"
      whileInView="visible"
      viewport={{ amount: 0.5, once: true }}
      options={{ type: "chars,lines", mask: "lines" }}
    >
      <h1>Beautiful scroll-triggered reveal with per-line stagger</h1>
    </SplitText>
  );
}

Props Reference

Inherits all props from React SplitText, plus:
variants
Record<string, VariantDefinition>
Named variant definitions
initial
string | VariantDefinition | false
Initial variant (applied instantly on mount)
animate
string | VariantDefinition
Variant to animate to after mount
whileInView
string | VariantDefinition
Variant to animate to when entering viewport
whileOutOfView
string | VariantDefinition
Variant to animate to when leaving viewport
whileScroll
string | VariantDefinition
Variant driven by scroll progress
whileHover
string | VariantDefinition
Variant to animate to on hover
whileTap
string | VariantDefinition
Variant to animate to on tap/click
whileFocus
string | VariantDefinition
Variant to animate to on focus
exit
string | VariantDefinition | false
Exit variant for AnimatePresence
transition
AnimationOptions
Global transition options
delayScope
'global' | 'local'
default:"global"
How delay functions resolve indices
custom
TCustom
Custom data passed to function variants
reducedMotion
'user' | 'always' | 'never'
default:"user"
Reduced motion handling
animateOnResplit
boolean
default:false
Re-run initial->animate when autoSplit triggers

React

Callback-based React component

Morph

Text morphing transitions

Kerning

How kerning compensation works

Performance

Optimization tips

Build docs developers (and LLMs) love