Splitting Text

PreviousNext

A splitting text animation.

Docs
animate-uiui

Preview

Loading preview…
registry/primitives/texts/splitting/index.tsx
'use client';

import * as React from 'react';
import {
  motion,
  type Variants,
  type TargetAndTransition,
  type HTMLMotionProps,
  type Transition,
} from 'motion/react';

import {
  useIsInView,
  type UseIsInViewOptions,
} from '@/hooks/use-is-in-view';

type DefaultSplittingTextProps = Omit<
  HTMLMotionProps<'div'>,
  'children' | 'initial' | 'animate' | 'transition'
> & {
  initial?: TargetAndTransition;
  animate?: TargetAndTransition;
  transition?: Transition;
  stagger?: number;
  delay?: number;
  disableAnimation?: boolean;
} & UseIsInViewOptions;

type CharsOrWordsSplittingTextProps = DefaultSplittingTextProps & {
  type?: 'chars' | 'words';
  text: string;
};

type LinesSplittingTextProps = DefaultSplittingTextProps & {
  type?: 'lines';
  text: string[];
};

type SplittingTextProps =
  | CharsOrWordsSplittingTextProps
  | LinesSplittingTextProps;

const SplittingText: React.FC<SplittingTextProps> = ({
  ref,
  text,
  type = 'chars',
  initial = { x: 150, opacity: 0 },
  animate = { x: 0, opacity: 1 },
  transition = { duration: 0.7, ease: 'easeOut' },
  stagger,
  delay = 0,
  inView = false,
  inViewMargin = '0px',
  inViewOnce = true,
  disableAnimation = false,
  ...props
}) => {
  const containerVariants: Variants = {
    hidden: {},
    visible: {
      transition: {
        delayChildren: delay / 1000,
        staggerChildren:
          stagger ?? (type === 'chars' ? 0.05 : type === 'words' ? 0.2 : 0.3),
      },
    },
  };

  const itemVariants: Variants = {
    hidden: disableAnimation ? animate : initial,
    visible: {
      ...animate,
      transition: disableAnimation ? { duration: 0 } : transition,
    },
  };

  const { ref: localRef, isInView } = useIsInView(
    ref as React.Ref<HTMLElement>,
    {
      inView,
      inViewOnce,
      inViewMargin,
    },
  );

  if (Array.isArray(text)) {
    return (
      <motion.span
        ref={localRef}
        initial="hidden"
        animate={isInView ? 'visible' : 'hidden'}
        variants={containerVariants}
        {...props}
      >
        {text.map((line, i) => (
          <React.Fragment key={`line-${i}`}>
            <motion.span
              variants={itemVariants}
              style={{ display: 'inline-block' }}
            >
              {line}
            </motion.span>
            {i < text.length - 1 ? <br /> : null}
          </React.Fragment>
        ))}
      </motion.span>
    );
  }

  if (type === 'words') {
    const tokens = (text as string).match(/\S+\s*/g) || [];
    return (
      <motion.span
        ref={localRef}
        initial="hidden"
        animate={isInView ? 'visible' : 'hidden'}
        variants={containerVariants}
        {...props}
      >
        {tokens.map((token, i) => (
          <React.Fragment key={i}>
            <motion.span
              variants={itemVariants}
              style={{ display: 'inline-block', whiteSpace: 'normal' }}
            >
              {token.trim()}
            </motion.span>
            {/\s$/.test(token) ? ' ' : null}
          </React.Fragment>
        ))}
      </motion.span>
    );
  }

  const tokens = (text as string).split(/(\s+)/);
  const perChar = stagger ?? 0.05;
  const baseDelaySec = (delay ?? 0) / 1000;

  let globalIndex = 0;

  return (
    <motion.span
      ref={localRef}
      initial="hidden"
      animate={isInView ? 'visible' : 'hidden'}
      variants={{
        hidden: {},
        visible: { transition: {} },
      }}
      {...props}
    >
      {tokens.map((tok, wi) => {
        if (/^\s+$/.test(tok)) {
          return <span key={`space-${wi}`}>{tok}</span>;
        }
        const chars = Array.from(tok);
        const wordDelay = baseDelaySec + perChar * globalIndex;
        globalIndex += chars.length;

        return (
          <motion.span
            key={`word-${wi}`}
            style={{ display: 'inline-block', whiteSpace: 'nowrap' }}
            variants={{}}
            transition={{ delayChildren: wordDelay, staggerChildren: perChar }}
            initial="hidden"
            animate={isInView ? 'visible' : 'hidden'}
          >
            {chars.map((ch, ci) => (
              <motion.span
                key={`ch-${wi}-${ci}`}
                variants={itemVariants}
                style={{ display: 'inline-block', whiteSpace: 'pre' }}
              >
                {ch}
              </motion.span>
            ))}
          </motion.span>
        );
      })}
    </motion.span>
  );
};

export { SplittingText, type SplittingTextProps };

Installation

npx shadcn@latest add @animate-ui/primitives-texts-splitting

Usage

import { PrimitivesTextsSplitting } from "@/components/ui/primitives-texts-splitting"
<PrimitivesTextsSplitting />