Sei un senior frontend engineer. Stai lavorando su un sito Next.js 16 + React 19 + Tailwind v4 in italiano, look chanhdai-inspired: colonna stretta 672px, Geist Sans + Geist Mono, hairline 1px, divisori a stripe diagonale, palette zinc.
Token CSS disponibili: --bg, --bg-alt, --fg, --fg-muted, --fg-soft, --border, --border-strong, --accent. Usa SEMPRE queste variabili tramite le utility tailwind generate (bg-bg, text-fg-muted, border-border, ecc.). Helper "cn" da "@/lib/utils". Niente librerie UI extra: solo lucide-react e tailwind-merge.
Genera un componente <TextReveal> word-by-word reveal su scroll.
Props:
- children: string.
- className?: string.
Implementazione:
- "use client". useRef<HTMLParagraphElement>, useState<number> progress 0–1.
- useEffect: handler scroll passive che legge getBoundingClientRect → calcola t = (rect.top - end) / (start - end), progress = clamp(0, 1, 1 - t). Anche listener resize.
- words = children.split(/\s+/). reachedIndex = floor(progress * words.length).
- Render <p ref={ref}>: per ogni word, span con color = lit ? var(--fg) : var(--fg-soft), transition-colors duration-300 var(--ease-out).
Output: file completo src/components/text-reveal.tsx.Component
LiveNewText Reveal
Listener scroll passive. Calcola progress in base alla posizione del rect rispetto al viewport. Niente IntersectionObserver.
Esempio01
Le parole si accendono una alla volta mentre scorri la pagina, trasformando un blocco di testo lungo in un piccolo viaggio editoriale.
Il design è la cosa che si vede quando si toglie tutto il superfluo, e ciò che resta deve guadagnarsi il proprio posto. Ogni componente di questo catalogo prova a rispettare questa regola.
Niente librerie pesanti. Solo CSS, design tokens, e una selezione attenta di animazioni motivate.
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
export type TextRevealProps = {
children: string;
className?: string;
};
/**
* <TextReveal/> — splits `children` into words and lerps each one's color
* from `--fg-soft` to `--fg` based on scroll progress through the container.
* Word-by-word reveal driven by `scroll` listener (passive) — no IntersectionObserver
* is necessary because the math is purely about viewport overlap.
*/
export function TextReveal({ children, className }: TextRevealProps) {
const ref = useRef<HTMLParagraphElement | null>(null);
const [progress, setProgress] = useState(0);
const words = children.split(/\s+/).filter(Boolean);
useEffect(() => {
const el = ref.current;
if (!el) return;
const update = () => {
const rect = el.getBoundingClientRect();
const viewport = window.innerHeight || 1;
const start = viewport;
const end = -rect.height;
const t = (rect.top - end) / (start - end);
const clamped = Math.min(1, Math.max(0, 1 - t));
setProgress(clamped);
};
update();
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
return () => {
window.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, []);
const reachedIndex = Math.floor(progress * words.length);
return (
<p
ref={ref}
className={cn(
"text-2xl leading-relaxed font-medium tracking-tight",
className,
)}
>
{words.map((word, i) => {
const lit = i < reachedIndex;
return (
<span
key={`${word}-${i}`}
className="inline-block transition-colors duration-300 [transition-timing-function:var(--ease-out)]"
style={{ color: lit ? "var(--fg)" : "var(--fg-soft)" }}
>
{word}
{i < words.length - 1 ? " " : ""}
</span>
);
})}
</p>
);
}
Note — Funziona meglio in un blocco di testo lungo (almeno 30+ parole) e richiede che il container abbia altezza maggiore del viewport perché il progress si veda.
Prompt LLM02
Incolla in Claude o ChatGPT per generare la tua variante. Include il contesto del brand, i token e i vincoli del progetto.
Uso tipico03
<TextReveal>Long form text che si illumina con lo scroll della pagina.</TextReveal>Dipendenze04
- tailwind-merge
- clsx
- @/lib/utils#cn
Ti è servito? Dimmelo, oppure proponi il prossimo componente.
