Text & Numbers

Text motion

Splits a string into characters, words or lines and staggers a preset reveal across them.

Takes a plain string and splits it into segments — characters, words or lines — then reveals each one in sequence. It is the general-purpose version of the family: if you want a specific motion (blur, scramble, roll, wave) there is a dedicated component, and this is the one to reach for when the difference between those motions does not matter.

Two independent rates shape the result. speedReveal sets the stagger between segments — the built-in defaults are 0.03s per character, 0.05s per word and 0.1s per line, divided by speedReveal. speedSegment sets how long each segment takes on its own. Text that should feel punchy wants a high speedSegment and a low speedReveal; text that should feel deliberate wants the opposite.

The character mode groups by word so that lines still wrap at word boundaries. Each character gets its own overflow: hidden wrapper, which is what lets a y-based preset appear to rise out of the baseline rather than slide as a whole paragraph.

Usage

Pass the text as children and pick a segmentation. The component renders as as (a p by default) and passes class to that element, so it drops into existing copy without a wrapper.

Split the string, then stagger the reveal.

<template>
  <MTextEffect
    children="Split the string, then stagger the reveal."
    per="char"
    preset="blur"
    as="p"
    class="text-xl font-semibold text-default"
  />
</template>

Examples

Switch the segmentation and preset

char · blur

Split the string, then stagger the reveal.
<template>
  <div class="flex w-full max-w-md flex-col gap-5">
    <div class="rounded-2xl border border-default bg-default p-6">
      <p class="mb-3 text-xs font-medium uppercase tracking-widest text-muted">
        {{ per }} · {{ preset }}
      </p>
      <MTextEffect
        :key="run"
        :children="copy"
        :per="per"
        :preset="preset"
        as="span"
        class="text-xl font-semibold text-default"
        :speed-reveal="1.4"
      />
    </div>

    <div class="grid grid-cols-2 gap-3">
      <USelect
        v-model="per"
        :items="perOptions"
      />
      <USelect
        v-model="preset"
        :items="presetOptions"
      />
    </div>

    <UButton
      label="Replay"
      icon="i-lucide-rotate-ccw"
      variant="outline"
      class="justify-center"
      @click="run++"
    />
  </div>
</template>

<script setup lang="ts">
const copy = 'Split the string, then stagger the reveal.'
const run = ref(0)
const per = ref<'word' | 'char' | 'line'>('char')
const preset = ref<'blur' | 'fade-in-blur' | 'scale' | 'fade' | 'slide'>('blur')

const perOptions = ['char', 'word', 'line']
const presetOptions = ['blur', 'fade-in-blur', 'scale', 'fade', 'slide']
</script>

Control the timing

speedReveal sets the gap between segments while speedSegment sets how long each segment takes, and delay holds the whole reveal back. Combine them with per="line" for a deliberate, multi-line statement; trigger hides the tree entirely instead of freezing it.

Build faster.Ship sooner.Sleep better.

<template>
  <div class="flex w-full max-w-md flex-col gap-6">
    <div class="rounded-2xl border border-default bg-default p-6">
      <MTextEffect
        :key="run"
        :children="copy"
        per="line"
        preset="fade-in-blur"
        as="h2"
        class="text-2xl font-semibold leading-snug text-default"
        :delay="0.4"
        :speed-reveal="3"
        :speed-segment="0.5"
        :trigger="playing"
      />
    </div>

    <div class="flex items-center justify-center gap-3">
      <UButton
        label="Replay"
        icon="i-lucide-rotate-ccw"
        variant="outline"
        @click="run++"
      />
      <UButton
        :label="playing ? 'Hide' : 'Show'"
        :icon="playing ? 'i-lucide-eye-off' : 'i-lucide-eye'"
        variant="outline"
        @click="playing = !playing"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
const run = ref(0)
const playing = ref(true)
const copy = 'Build faster.\nShip sooner.\nSleep better.'
</script>

Custom variants and transitions

variants replaces the generated ones, so supply your own container stagger and item hidden/visible pair. containerTransition and segmentTransition merge onto the container and each segment respectively, and segmentWrapperClassName reshapes the per-word wrapper that char mode introduces.

Custom variants
<template>
  <div class="flex w-full max-w-md flex-col gap-4">
    <MTextEffect
      :key="run"
      :children="copy"
      per="char"
      as="span"
      class="text-3xl font-black uppercase tracking-tight text-primary"
      :variants="{
        container: {
          hidden: { opacity: 1 },
          visible: { opacity: 1, transition: { staggerChildren: 0.025 } },
        },
        item: {
          hidden: { opacity: 0, y: '0.5em' },
          visible: { opacity: 1, y: 0 },
        },
      }"
      :container-transition="{ duration: 0.3 }"
      :segment-transition="{ type: 'spring', stiffness: 420, damping: 24 }"
      segment-wrapper-class-name="overflow-visible"
    />

    <UButton
      label="Replay"
      icon="i-lucide-rotate-ccw"
      variant="outline"
      class="self-center"
      @click="run++"
    />
  </div>
</template>

<script setup lang="ts">
const run = ref(0)
const copy = 'Custom variants'
</script>

Customize

Split the string, then stagger the reveal.

Customize

ChildrenSplit the string, then stagger the reveal.
Perchar
Presetblur
Asp
Delay (s)0
Speed reveal1
Speed segment1
Trigger
Segment wrapper class

Props

Prop Type Default Description
childrenstring''The text to animate. It is split on whitespace for word, on newlines for line, and per character for char — there is no slot, so a multi-line string in line mode needs real \n characters.
per'word' | 'char' | 'line''word'Segmentation unit. Each unit sets its own default stagger: 0.03s for a character, 0.05s for a word, 0.1s for a line.
preset'blur' | 'fade-in-blur' | 'scale' | 'fade' | 'slide''fade'The motion applied to each segment. fade is opacity only; slide and fade-in-blur travel 20px upward and blur/fade-in-blur resolve a 12px filter. An unrecognised value falls back to fade rather than throwing.
asstring'p'Element to render as.
delaynumber0Seconds before the first segment animates, applied as the container's delayChildren.
speedRevealnumber1Multiplier on the between-segment stagger. Higher is tighter. A value of 0 collapses the stagger to zero and the segments animate together.
speedSegmentnumber1Multiplier on each segment's own duration, which the source derives as (1 / speedSegment) * 0.4 seconds.
triggerbooleantrueWhether the animated tree renders at all. Set it to false and the root element is empty — the text does not appear as plain text — so it is a switch for "show the animation" versus "show nothing", not a way to disable motion. To replay, remount the component (a changing :key is enough).
classstring-Merged onto the root element, which carries overflow: hidden, display: inline-flex and flex-wrap: wrap inline so a char preset cannot leak outside the element.
variants{ container?: Record<string, any>, item?: Record<string, any> }-Motion variants that replace the generated ones. container replaces the stagger orchestration entirely — supply your own staggerChildren if you do, since the computed speedReveal and delay no longer apply. item replaces the preset's hidden/visible pair.
containerTransitionany-Transition merged onto the container Motion. Only reaches the container, not the segments.
segmentTransitionany-Transition merged onto every segment Motion, applied after the preset's own duration, so it wins where the two overlap.
segmentWrapperClassNamestring-Class on the per-word wrapper that char mode introduces around each word. The wrapper is what carries overflow: hidden in char mode; changing it away from that can make slide and fade-in-blur look like they slide in from nowhere.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.textEffect in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-
container-
segmentWrapper-
segment-

Accessibility

The component renders ordinary text inside a real element, so a screen reader announces it as text and it needs no ARIA. Two caveats come from the DOM shape: each segment is its own element, so a copied selection carries one element per character in char mode, and trigger="false" removes the content from the accessibility tree entirely rather than rendering it statically.

There is no reduced-motion handling in the source. Under prefers-reduced-motion: reduce the reveal still plays. Set trigger="false" and render your own plain text if you need a non-animated fallback.

Performance

  • Segments are computed once per children/per change, not per frame; the animation runs on transforms and opacity.
  • char mode builds one extra wrapper element per word, so the element count is roughly characters + words. word mode is the cheapest option; char on a long paragraph is not.
  • The blur presets animate a filter, which is composited but not free — a long string in blur or fade-in-blur does more work per frame than fade.
  • Changing speedReveal or speedSegment recomputes the variants and the stagger, but no animation state is reset, so it can be driven live.
speedSegment is not "speed" in the intuitive sense. It only sets each segment's own duration, while speedReveal sets the gap between segments — so raising speedSegment makes every segment take longer but the overall run longer and more deliberate, not faster. Raise speedReveal when you want it to feel brisk.
Copyright © 2026