Morphing Text

Text that morphs from one string to the next, letters gliding to their new place.

<script setup lang="ts">
import { MorphingText } from '@animate-ui/vue/primitives/texts/morphing'

withDefaults(
  defineProps<{ loop?: boolean | string, holdDelay?: number | string }>(),
  { loop: true, holdDelay: 2500 },
)

const texts = [
  'MorphingText Primitive',
  'Animate your text 🚀',
  'Handles emojis 🚀✨',
  'Built with Motion ✨',
]
</script>

<template>
  <MorphingText
    :key="`${loop}-${holdDelay}`"
    class="max-w-2xl text-4xl font-semibold"
    :text="texts"
    :loop="loop === true || loop === 'true'"
    :hold-delay="Number(holdDelay)"
  />
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import { MorphingText } from '@animate-ui/vue/primitives/texts/morphing';
</script>

<template>
  <MorphingText :text="['Fast', 'Accessible', 'Animated']" loop />
</template>

Letters are animated one by one, so a line can break inside a word. Prefer texts short enough not to wrap.

Animation

  • Each text holds for hold-delay, then morphs into the next. The letters both texts share, in the same order, slide to their new place on spring.selection (stiffness 300, damping 30): they never cross each other. The others fade out into a blur (6px) in 150ms, and the new ones rise in from it (0.12em, 300ms on the ease-out curve), 80ms later and 12ms apart.
  • Each letter takes exactly its width in the whole text, so the font's kerning holds: at rest, the text reads as if it were set in one piece.
  • Emoji and accented letters stay whole.
  • The cycle pauses off screen and in a hidden tab.
  • <AnimateUIProvider :tempo> paces the slide, the fades and the delays.
  • Under reduced motion, the texts keep cycling as a plain crossfade.
  • Screen readers read the current text whole, not letter by letter. It is not a live region.

API Reference

MorphingText

PropTypeDefault
text
string | string[]-
loop?
booleanfalse
hold-delay?
number2500
delay?
number0
initial?
TargetAndTransition | VariantLabels | boolean{ opacity: 0, y: '0.12em', filter: 'blur(6px)' }
animate?
TargetAndTransition | VariantLabels | boolean{ opacity: 1, y: 0, filter: 'blur(0px)' }
exit?
TargetAndTransition | VariantLabels{ opacity: 0, filter: 'blur(6px)' }, in 150ms
variants?
Variants-
transition?
Transition{ type: 'spring', stiffness: 300, damping: 30, opacity and filter: { duration: 0.3, ease: [0.23, 1, 0.32, 1] } }
in-view?
booleanfalse
in-view-margin?
string'0px'
in-view-once?
booleantrue
...props?
Omit<HTMLMotionProps<'span'>, 'children'>-

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026