useSplitText
useSplitText wraps the splitText
function from AnimeJS. It breaks text into individual lines, words, or characters
wrapped in HTML span elements so they can be animated independently.
When target elements or parameters change, nanime cleans up split elements and
re-splits the text while preserving registered effects across rebuilds.
Usage
import { stagger } from '#nanime/utils'
const text = useTemplateRef('phrase')
const { chars } = useSplitText(text, {
chars: true,
})
useWaapiAnimate(chars, {
y: ['-30%', 0],
delay: stagger(50),
duration: 1000,
ease: 'outBounce',
alternate: true,
loop: true,
})
<template>
<span ref="phrase" class="text-xl text-default font-mono">
Hello World!
</span>
</template>
Signature
function useSplitText(
target: MaybeRef<SplitTextTargets>,
parameters?: MaybeRefOrGetter<TextSplitterParams>,
options?: SplitTextOptions
): SplitText
Parameters
{ chars: true }, a ref or a getter. The
text is split again when a value in it changes. A new object with the same
values keeps the current split, but an inline function such as
chars: (el) => ... counts as a change on every render. See the
AnimeJS splitText documentation
for the options.html takes MaybeRefOrGetter<string | null | undefined> and
defaults to the target's own content. It is the text to split. When it
changes, the new text is split in place. While it is null or undefined,
the current text stays and a warning is logged in development. Render the
element with v-once when you use it.Returns
Returns a plain object of refs and computed getters. Destructuring is safe.
| Member | Type | Notes |
|---|---|---|
lines | Ref<HTMLElement[]> | The line elements, empty until mount. |
words | Ref<HTMLElement[]> | The word elements, empty until mount. |
chars | Ref<HTMLElement[]> | The character elements, empty until mount. |
properties | ComputedRef | Non-function properties of the underlying splitter instance, except lines, words and chars. undefined before mount. |
methods | ComputedRef<SplitterMethods> | Splitter methods (revert, split, refresh), with addEffect wrapped to persist. undefined before mount. |
refresh() | () => void | Splits the text again after you change it by hand. |
Caveats
Arrays are empty during setup
// wrong: chars.value is still [] during setup
const heading = useTemplateRef('heading')
const { chars } = useSplitText(heading, { chars: true })
console.log(chars.value.length) // 0
// right: pass the ref directly to an animation composable
useAnimate(chars, { opacity: [0, 1], delay: stagger(40) })
DOM elements do not exist until the component mounts, so lines, words, and
chars stay empty during setup. Passing .value immediately reads an empty
array and attaches zero animation targets. Pass the refs directly to animation
composables or inspect them inside a watcher after mount.
Render changing text with v-once
<script setup lang="ts">
const heading = useTemplateRef('heading')
const name = ref('Ada')
const html = computed(() => `Hello <strong>${name.value}</strong>`)
const { chars } = useSplitText(heading, { chars: true }, { html })
</script>
<template>
<!-- wrong: Vue patches text that the split already replaced -->
<h1 ref="heading">Hello <strong>{{ name }}</strong></h1>
<!-- right: Vue renders the text once, the html option handles changes -->
<h1 ref="heading" v-once v-html="html" />
</template>
AnimeJS keeps the text of a split in its html property and puts it back on
every split. Text that Vue re-renders inside the element is lost on the next
split. With v-once, Vue renders the first text, on the server too, and never
touches the element again. The html option then sets that property and
splits again when it changes, as the AnimeJS
refresh() docs
describe.
Manual content updates require refresh
// wrong: innerHTML changes are not detected automatically
heading.value.textContent = 'Updated message'
// right: call refresh to re-split updated text
heading.value.textContent = 'Updated message'
refresh()
nanime does not attach DOM mutation observers to split elements. When you
manually mutate text content or update elements without changing the composable
parameters, the split structure remains stale. Call refresh() after modifying
text content to force an immediate re-split of the new text.
Type reference
type SplitTargets = HTMLElement | string
type SplitTextTargets = SplitTargets | MaybeElementRef<HTMLElement> | null | undefined | MaybeElementRef<VueInstance>
type SplitEffect = Parameters<TextSplitter['addEffect']>[0]
type SplitterMethods = Omit<OnlyFunctionProperties<TextSplitter>, 'addEffect'> & {
addEffect: (effect: SplitEffect) => void
}
interface SplitTextOptions {
html?: MaybeRefOrGetter<string | null | undefined>
}
type SplitText = {
lines: Ref<HTMLElement[]>
words: Ref<HTMLElement[]>
chars: Ref<HTMLElement[]>
properties: ComputedRef<Omit<NonFunctionProperties<TextSplitter>, 'lines' | 'words' | 'chars'> | undefined>
methods: ComputedRef<SplitterMethods | undefined>
refresh: () => void
}
See also
useScrambleText: random character scramble effectuseAnimate: main-thread animation engine for stagger effects- AnimeJS text documentation: upstream splitting options and templates