Text Effects
LetterFlow
Letters rise out from under an invisible line and tilt upright as they land, in one wave across the whole line. Words never break mid-way.
Download
- React
- Tailwind CSS v4
- motion
components/ui/letter-flow.tsx
"use client";
import { motion, useReducedMotion } from "motion/react";
type LetterFlowProps = {
text: string;
className?: string;
/** Seconds between one letter and the next. */
stagger?: number;
/** Play only the first time it scrolls into view. */
once?: boolean;
};
/* LetterFlow: letters rise out from under an invisible line, tilting upright as they land, in one flowing wave. */
export function LetterFlow({ text, className = "", stagger = 0.025, once = true }: LetterFlowProps) {
const reduce = useReducedMotion();
if (reduce) return <span className={className}>{text}</span>;
const words = text.split(" ");
// Each letter's place in the whole text, so the wave carries on across words.
const starts = words.map((_, w) => words.slice(0, w).join("").length);
return (
<motion.span className={className} initial="hidden" whileInView="show" viewport={{ once, amount: 0.6 }}>
<span className="sr-only">{text}</span>
<span aria-hidden>
{words.map((word, w) => (
<span key={w}>
{/* Each word clips its own letters and never breaks across lines. */}
<span className="-mb-[0.15em] inline-block overflow-hidden whitespace-nowrap pb-[0.15em] align-bottom">
{[...word].map((ch, i) => (
<motion.span
key={i}
className="inline-block origin-bottom-left"
custom={starts[w] + i}
variants={{
hidden: { y: "110%", rotate: 12 },
show: (k: number) => ({
y: 0,
rotate: 0,
transition: { type: "spring", stiffness: 260, damping: 24, delay: k * stagger },
}),
}}
>
{ch}
</motion.span>
))}
</span>
{w < words.length - 1 && " "}
</span>
))}
</span>
</motion.span>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/letter-flow.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/letter-flow-demo.tsx
"use client"; import { useState } from "react"; import { LetterFlow } from "@/components/ui/letter-flow"; export default function LetterFlowExample() { const [run, setRun] = useState(0); return ( <div className="grid h-full place-items-center bg-[#0f1a17] p-6"> <div className="max-w-xl text-center"> <LetterFlow key={run} text="Motion with a purpose" className="text-[clamp(28px,5vw,56px)] font-bold leading-tight tracking-tight text-[#d9fbe9]" /> <button type="button" onClick={() => setRun((r) => r + 1)} className="mt-6 block w-full text-xs text-[#d9fbe9]/50 hover:text-[#d9fbe9]"> Replay </button> </div> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| text | string | — | Text to show. |
| stagger | number | 0.025 | Seconds between letters. |
| once | boolean | true | Play only the first time it scrolls into view. |
| className | string | "" | Type styles for the text. |