John Doe
john@example.com
LetterDraw is a customizable SVG text animation that reveals text using a stroke-draw effect. It’s theme-aware, responsive, and ideal for hero sections or modern UI headings.
Install the required dependencies:
npm install next-themesimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
"use client";
import { useTheme } from "next-themes";
import React, { useRef, useEffect, useId, useState } from "react";
import gsap from "gsap";
interface LetterDrawProps {
text?: string;
fontSize?: number;
strokeColor?: string;
strokeWidth?: number;
duration?: number;
glow?: boolean;
}
export default function LetterDraw({
text = "Fork UI",
fontSize = 180,
strokeColor,
strokeWidth = 4,
duration = 1.2,
glow = true,
}: LetterDrawProps) {
const { resolvedTheme } = useTheme();
const svgRef = useRef<SVGSVGElement | null>(null);
const letterRefs = useRef<(SVGTextElement | null)[]>([]);
const gradientId = useId();
const [positions, setPositions] = useState<number[]>([]);
useEffect(() => {
const setup = async () => {
await document.fonts.ready;
let totalWidth = 0;
const widths: number[] = [];
letterRefs.current.forEach((letter) => {
if (!letter) return;
const w = letter.getComputedTextLength();
widths.push(w);
totalWidth += w;
});
let startX = -totalWidth / 2;
const pos = widths.map((w) => {
const x = startX + w / 2;
startX += w;
return x;
});
setPositions(pos);
const tl = gsap.timeline({
repeat: -1,
repeatDelay: 1,
});
letterRefs.current.forEach((letter, i) => {
if (!letter) return;
const length = letter.getComputedTextLength() * 10;
gsap.set(letter, {
strokeDasharray: length,
strokeDashoffset: length,
y: 40,
opacity: 0,
});
tl.to(
letter,
{
strokeDashoffset: 0,
y: 0,
opacity: 1,
duration,
ease: "power3.out",
},
i * 0.15,
);
});
// reset letters after drawing
tl.to(
letterRefs.current,
{
strokeDashoffset: (i, target: any) =>
target.getComputedTextLength() * 10,
opacity: 0,
y: 40,
duration: 0.6,
ease: "power2.in",
},
"+=1",
);
};
setup();
}, [text, duration]);
useEffect(() => {
const svg = svgRef.current;
if (!svg) return;
const handleMove = (e: MouseEvent) => {
const rect = svg.getBoundingClientRect();
const x = (e.clientX - rect.left - rect.width / 2) / 20;
const y = (e.clientY - rect.top - rect.height / 2) / 20;
gsap.to(svg, {
rotateY: x,
rotateX: -y,
transformPerspective: 800,
transformOrigin: "center",
ease: "power2.out",
duration: 0.5,
});
};
const reset = () => {
gsap.to(svg, {
rotateX: 0,
rotateY: 0,
duration: 0.8,
ease: "power3.out",
});
};
svg.addEventListener("mousemove", handleMove);
svg.addEventListener("mouseleave", reset);
return () => {
svg.removeEventListener("mousemove", handleMove);
svg.removeEventListener("mouseleave", reset);
};
}, []);
const finalStroke =
strokeColor ||
`url(#${resolvedTheme === "dark" ? gradientId + "-dark" : gradientId})`;
return (
<svg
ref={svgRef}
viewBox="-600 -200 1200 400"
className="w-full h-auto cursor-pointer"
preserveAspectRatio="xMidYMid meet"
style={{ transformStyle: "preserve-3d" }}
>
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%">
<stop offset="0%" stopColor="#000" />
<stop offset="100%" stopColor="#555" />
</linearGradient>
<linearGradient id={`${gradientId}-dark`} x1="0%" y1="0%" x2="100%">
<stop offset="0%" stopColor="#fff" />
<stop offset="100%" stopColor="#aaa" />
</linearGradient>
{glow && (
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
)}
</defs>
<g
textAnchor="middle"
dominantBaseline="middle"
fontSize={fontSize}
fontFamily="inherit"
>
{text.split("").map((char, i) => (
<text
key={i}
ref={(el) => {
letterRefs.current[i] = el;
}}
x={positions[i] || 0}
y={0}
fill="transparent"
stroke={finalStroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
filter={glow ? "url(#glow)" : undefined}
className="select-none font-bebas tracking-widest"
>
{char}
</text>
))}
</g>
</svg>
);
}
Usecase
* LetterDraw is an animated SVG text component that creates a smooth stroke-drawing effect, making text appear as if it’s being written in real time.