Skip to main content
The createSplitClones() function from griffo/helpers creates clone layers from split text output. This is useful for advanced reveal effects, swap animations, and layered text transitions.

Import

import { createSplitClones } from "griffo/helpers";

Basic usage

import { splitText } from "griffo";
import { createSplitClones } from "griffo/helpers";
import { animate, stagger } from "motion";

const split = splitText(element, { type: "chars", mask: "chars" });
const layers = createSplitClones(split, { unit: "chars", wrap: true });

// Animate originals and clones
animate(
  layers.originals,
  { y: ["0%", "-100%"] },
  { delay: stagger(0.02) }
);

animate(
  layers.clones,
  { y: ["100%", "0%"] },
  { delay: stagger(0.02) }
);

Function signature

function createSplitClones(
  split: SplitTextResult,
  options: CreateSplitClonesOptions
): CreateSplitClonesResult

Parameters

split
SplitTextResult
required
The result object from a splitText() call.
const split = splitText(element, { type: "chars" });
const layers = createSplitClones(split, { unit: "chars" });
options
CreateSplitClonesOptions
required
Configuration for clone creation.

Options

options.unit
'chars' | 'words' | 'lines'
required
Which split unit to clone.
createSplitClones(split, { unit: "chars" })
createSplitClones(split, { unit: "words" })
createSplitClones(split, { unit: "lines" })
options.wrap
boolean
default:false
Whether to wrap each original element in a track container.
createSplitClones(split, {
  unit: "chars",
  wrap: true // Creates track wrappers with position: relative
})
When true, each original is wrapped in a <span> with position: relative, and clones are positioned absolutely within these tracks.
options.display
'auto' | 'inline-block' | 'block'
default:"'auto'"
Display style for track wrappers (only applies when wrap: true).
  • "auto" - Uses inline-block for chars/words, block for lines
  • "inline-block" - Force inline-block
  • "block" - Force block
createSplitClones(split, {
  unit: "words",
  wrap: true,
  display: "block"
})
options.cloneOffset
object
Position offset for clones relative to originals.
createSplitClones(split, {
  unit: "chars",
  wrap: true,
  cloneOffset: {
    axis: "y",        // "x" | "y"
    direction: "start", // "start" | "end"
    distance: "100%"  // Any CSS length
  }
})
Properties:
  • axis - Offset direction ("x" horizontal, "y" vertical)
  • direction - Offset side ("start" negative, "end" positive)
  • distance - Offset amount (CSS length)
Defaults: { axis: "y", direction: "start", distance: "100%" }
options.trackClassName
ClassInput
CSS class for track wrappers. Can be a string or function.
// Static
trackClassName: "clone-track"

// Function
trackClassName: ({ index, unit, original }) =>
  `track-${index}`
options.cloneClassName
ClassInput
CSS class for clone elements.
cloneClassName: ({ index }) => `clone clone-${index}`
options.trackStyle
StyleInput
Inline styles for track wrappers.
// Static
trackStyle: { overflow: "hidden" }

// Function
trackStyle: ({ index }) => ({
  overflow: "hidden",
  color: colors[index]
})
options.cloneStyle
StyleInput
Inline styles for clone elements.
cloneStyle: { color: "blue" }

Return value

unit
SplitUnit
The unit type that was cloned.
originals
HTMLSpanElement[]
Array of original split elements.
clones
HTMLSpanElement[]
Array of cloned elements.
tracks
HTMLSpanElement[]
Array of track wrapper elements (empty if wrap: false).
items
SplitCloneItem[]
Array of objects containing:
{
  index: number;
  original: HTMLSpanElement;
  clone: HTMLSpanElement;
  track: HTMLSpanElement | null;
}
cleanup
(options?) => void
Function to remove clones and tracks.
layers.cleanup(); // Remove clones only
layers.cleanup({ revertSplit: true }); // Remove clones and revert split

Examples

Vertical reveal

import { splitText } from "griffo";
import { createSplitClones } from "griffo/helpers";
import { animate, stagger } from "motion";

const split = splitText(element, { type: "words", mask: "words" });
const { originals, clones, cleanup } = createSplitClones(split, {
  unit: "words",
  wrap: true,
  cloneOffset: {
    axis: "y",
    direction: "start",
    distance: "100%"
  }
});

// Slide originals up and out
animate(originals, { y: ["0%", "-100%"] }, { delay: stagger(0.03) });

// Slide clones up into view
animate(clones, { y: ["100%", "0%"] }, { delay: stagger(0.03) }).finished.then(() => {
  cleanup({ revertSplit: true });
});

Horizontal swap

const split = splitText(element, { type: "chars" });
const { originals, clones, cleanup } = createSplitClones(split, {
  unit: "chars",
  wrap: true,
  cloneOffset: {
    axis: "x",
    direction: "end",
    distance: "100%"
  },
  cloneStyle: { color: "blue" }
});

animate(originals, { x: ["-100%"], opacity: [0] }, { delay: stagger(0.02) });
animate(clones, { x: ["100%", "0%"], opacity: [0, 1] }, { delay: stagger(0.02) });

Custom styling per clone

const colors = ["red", "orange", "yellow", "green", "blue"];

const layers = createSplitClones(split, {
  unit: "chars",
  wrap: true,
  cloneStyle: ({ index }) => ({
    color: colors[index % colors.length]
  })
});

Type definitions

type SplitUnit = "chars" | "words" | "lines";

type StyleInput =
  | Partial<CSSStyleDeclaration>
  | ((ctx: {
      index: number;
      unit: SplitUnit;
      original: HTMLSpanElement;
    }) => Partial<CSSStyleDeclaration>);

type ClassInput =
  | string
  | ((ctx: {
      index: number;
      unit: SplitUnit;
      original: HTMLSpanElement;
    }) => string | undefined);

interface CreateSplitClonesOptions {
  unit: SplitUnit;
  wrap?: boolean;
  display?: "auto" | "inline-block" | "block";
  cloneOffset?: {
    axis?: "x" | "y";
    direction?: "start" | "end";
    distance?: string;
  };
  trackClassName?: ClassInput;
  cloneClassName?: ClassInput;
  trackStyle?: StyleInput;
  cloneStyle?: StyleInput;
}

interface SplitCloneItem {
  index: number;
  original: HTMLSpanElement;
  clone: HTMLSpanElement;
  track: HTMLSpanElement | null;
}

interface CreateSplitClonesResult {
  unit: SplitUnit;
  originals: HTMLSpanElement[];
  clones: HTMLSpanElement[];
  tracks: HTMLSpanElement[];
  items: SplitCloneItem[];
  cleanup: (options?: { revertSplit?: boolean }) => void;
}

Notes

  • Clones are inserted as siblings of the originals
  • When wrap: true, both original and clone are positioned within the track wrapper
  • The cleanup() function should be called when animations complete to remove clones and tracks
  • Pass { revertSplit: true } to cleanup to also revert the original split

Build docs developers (and LLMs) love