Text Roll
The TextRoll component gives each letter a 3D flip: it enters by rotating around the X axis and exits the same way, so the line appears to roll into place. It is a strong choice for a headline or label that should feel physical rather than a simple fade.
Usage
Pass the text as children. class is merged onto the root, and each letter rolls in with a per-character delay derived from getEnterDelay.
Discover Amazing Features
Our platform offers cutting-edge solutions that help you achieve your goals faster.
Blazing quick
Tuned for speed with minimal load times
Beautiful Effects
Stunning animations that captivate your audience
Plug-in ready
Quick to add with clean, well-documented code
<template>
<div class="p-8">
<div class="max-w-3xl mx-auto text-center">
<h1 class="text-4xl md:text-5xl font-bold mb-6">
Discover
<MTextRoll
children="Amazing Features"
class="text-primary"
/>
</h1>
<p class="text-xl text-muted mb-8">
Our platform offers
<MTextRoll
children="cutting-edge solutions"
class="text-purple-600 dark:text-purple-400 font-semibold"
/>
that help you achieve your goals faster.
</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12">
<UCard class="text-center">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 rounded-full bg-primary/10 dark:bg-primary/70 flex items-center justify-center">
<UIcon
name="i-lucide-zap"
class="w-8 h-8 text-primary"
/>
</div>
</div>
<h3 class="text-lg font-semibold mb-2">
<MTextRoll
children="Blazing quick"
class="text-primary"
/>
</h3>
<p class="text-muted text-sm">
Tuned for speed with minimal load times
</p>
</UCard>
<UCard class="text-center">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<UIcon
name="i-lucide-sparkles"
class="w-8 h-8 text-purple-600 dark:text-purple-400"
/>
</div>
</div>
<h3 class="text-lg font-semibold mb-2">
<MTextRoll
children="Beautiful Effects"
class="text-purple-600 dark:text-purple-400"
/>
</h3>
<p class="text-muted text-sm">
Stunning animations that captivate your audience
</p>
</UCard>
<UCard class="text-center">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center">
<UIcon
name="i-lucide-code"
class="w-8 h-8 text-green-600 dark:text-green-400"
/>
</div>
</div>
<h3 class="text-lg font-semibold mb-2">
<MTextRoll
children="Plug-in ready"
class="text-green-600 dark:text-green-400"
/>
</h3>
<p class="text-muted text-sm">
Quick to add with clean, well-documented code
</p>
</UCard>
</div>
<div class="mt-12">
<UButton
size="lg"
color="primary"
class="px-8 py-3 text-lg"
>
Explore Features
</UButton>
</div>
</div>
</div>
</template>
Examples
Rolling words
<template>
<div class="p-4">
<MTextRoll children="Scrolling words" />
</div>
</template>
Timing and custom variants
duration sets how long each letter takes to flip, while getEnterDelay and
getExitDelay stagger the letters. variants replaces the default rotateX
pair, and onAnimationComplete fires once after the last letter's exit — handy
for chaining the next roll.
<template>
<div class="flex w-full max-w-sm flex-col items-center gap-5">
<div class="flex items-baseline gap-2">
<span class="text-xs uppercase tracking-widest text-muted">Now playing</span>
<MTextRoll
:key="track"
:children="track"
:duration="0.6"
:get-enter-delay="i => i * 0.045"
:get-exit-delay="i => i * 0.045"
:transition="{ ease: [0.22, 1, 0.36, 1] }"
:variants="{
enter: { initial: { rotateX: 0, opacity: 1 }, animate: { rotateX: 95, opacity: 0 } },
exit: { initial: { rotateX: -95, opacity: 0 }, animate: { rotateX: 0, opacity: 1 } },
}"
class="text-lg font-semibold text-primary"
:on-animation-complete="() => (finished = true)"
/>
</div>
<div class="flex items-center gap-3">
<UButton
label="Next track"
icon="i-lucide-skip-forward"
variant="outline"
@click="next"
/>
<span class="text-xs text-dimmed">{{ finished ? 'ready' : 'rolling…' }}</span>
</div>
</div>
</template>
<script setup lang="ts">
const tracks = ['Midnight City', 'Reunion', 'Outro']
const index = ref(0)
const track = computed(() => tracks[index.value])
const finished = ref(false)
function next() {
finished.value = false
index.value = (index.value + 1) % tracks.length
}
</script>
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | '' | The text to roll. |
duration | number | 0.5 | Duration of each letter's enter and exit animation in seconds. |
getEnterDelay | (index: number) => number | (i) => i * 0.1 | Per-letter delay before the enter animation, in seconds. |
getExitDelay | (index: number) => number | (i) => i * 0.1 + 0.2 | Per-letter delay before the exit animation, in seconds. |
transition | object | { ease: 'easeIn' } | Transition merged over each letter's animation, after duration is applied. |
variants | object | undefined | Custom enter and exit variant pairs that replace the default 90° rotation. |
onAnimationComplete | () => void | undefined | Called once, after the last letter finishes its exit animation. |
class | string | undefined | CSS classes applied to the root element. |
ui | object | undefined | Per-slot classes merged over the theme defaults. See UI. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.textRoll in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | |
letter | - | relative inline-block [perspective:10000px] [transform-style:preserve-3d] [width:auto] |
enter | - | absolute inline-block [backface-visibility:hidden] [transform-origin:50%_25%] |
exit | - | absolute inline-block [backface-visibility:hidden] [transform-origin:50%_100%] |
placeholder | - | invisible |
srOnly | - | sr-only |
Accessibility
The roll is decorative: each letter is aria-hidden="true" and a visually hidden copy of the full children string fills the srOnly slot, so assistive technology reads the text once rather than letter by letter. The root is a plain span with no role or landmark.
Performance
- Each letter renders three animated
spans (enter, exit and aplaceholderthat reserves layout space), so a short string produces roughly three times the node count. Keepchildrento a word or short phrase. - The
placeholderkeeps the measured width stable while the enter/exit layers areabsolute, which avoids a layout shift during the roll. - There is no reduced-motion handling; the letters always roll.