Skip to content
Text & Typography Fancy Stable

TextRoll

.md

Single-line text that rolls to its new value per grapheme, odometer-style, whenever the value prop changes — while the real, unsplit text stays selectable and readable

Available for Svelte 5, React, and Vue 3.

Preview

Installation

pnpm add fancy-ui-svelte
import { TextRoll } from "fancy-ui-svelte";

Usage

svelte
<script lang="ts">
	import { TextRoll } from "fancy-ui-svelte";
	import { Switch } from "fancy-ui-svelte";

	let yearly = $state(false);
	// Same digit count either way ("$50" / "$40") on purpose — it's what makes
	// the roll visible as "only the changed digit moves": the "$" cell and the
	// trailing "0" cell keep their DOM node (same key, same grapheme), only
	// the middle digit's cell actually rolls.
	let price = $derived(yearly ? "$40" : "$50");
</script>

<div class="flex w-full flex-col items-center gap-6 p-6">
	<div
		class="bg-card border-border flex w-full max-w-xs flex-col items-center gap-1 rounded-xl border p-6"
	>
		<p class="text-muted-foreground text-sm">Pro plan</p>
		<p class="text-foreground flex items-baseline gap-1 text-4xl font-semibold">
			<!-- tabular: locks digit advance width so the two internal layers never
			     drift apart. live="polite": a final price after a toggle is exactly
			     the kind of change worth announcing (unlike a per-second ticker). -->
			<TextRoll value={price} tabular live="polite" />
			<span class="text-muted-foreground text-base font-normal">/mo</span>
		</p>
		<p class="text-muted-foreground text-xs">
			{yearly ? "Billed annually" : "Billed monthly"}
		</p>
	</div>

	<Switch bind:checked={yearly}>Bill yearly, save 20%</Switch>
</div>

Examples

Basic Usage

Auto Ticker

An auto-incrementing counter, only the changed digits roll.

Props

PropTypeDefaultDescription
value *string-The text to display; a change after mount triggers a per-grapheme roll to the new value
direction "auto" | "up" | "down""auto"Which way the cells travel; "auto" compares the trimmed old and new values as numbers and falls back to "up" for a tie, a non-numeric change, or an empty side
duration number300Roll duration in ms, collapsed to 0 under reduced motion
stagger number15Per-cell stagger step in ms, compressed to a 200ms total spread
from "first" | "last" | "center" | number"first"Stagger origin
tabular booleanfalsefont-variant-numeric: tabular-nums on both layers, to keep digit width locked
live "off" | "polite" | "assertive""off"Live-region role/aria-live pair on the real text layer; off by default
class string-Additional CSS classes
ref HTMLSpanElement | nullnullBindable reference to the root element