A name writes itself in a flowing hand, pen-stroke by pen-stroke, then finishes with a flourish.
Hizmedia
Customize
Usage
import { Mrs_Saint_Delafield } from "next/font/google";
import { Signature } from "@/components/motionkit/signature";
// any script font works; this one is on Google Fonts
const script = Mrs_Saint_Delafield({ weight: "400", subsets: ["latin"] });
<Signature
text="Hizmedia"
fontFamily={script.style.fontFamily}
className="text-7xl"
/>Motion
The exact curves this component runs, plotted with Motion's own maths.
Writing
Scrolls into view
Flourish
Last letter written
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/signature.tsx.
"use client";
import { useEffect, useRef, useState, type ElementType } from "react";
import { motion, useInView, useMotionTemplate, useMotionValue, useReducedMotion, animate } from "motion/react";
type SignatureProps = {
text: string;
/** A script font. Load it yourself, e.g. Mrs Saint Delafield from Google Fonts. */
fontFamily?: string;
color?: string;
/** Seconds for the pen to write the whole line. */
duration?: number;
/** Seconds to wait once in view. */
delay?: number;
/** Finish with a flourish drawn underneath. */
flourish?: boolean;
/** Element to render, e.g. "p". */
as?: ElementType;
className?: string;
};
export function Signature({
text,
fontFamily = "cursive",
color = "#F6F1E8",
duration = 2.2,
delay = 0,
flourish = true,
as: Tag = "p",
className = "",
}: SignatureProps) {
const ref = useRef<HTMLElement>(null);
const flourishRef = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const reduceMotion = useReducedMotion();
const [width, setWidth] = useState(0);
// how far the pen has travelled, as a percentage of the line
const pen = useMotionValue(reduceMotion ? 110 : -10);
// a soft edge just behind the pen, so letters appear as ink rather than wiping in like a curtain
const mask = useMotionTemplate`linear-gradient(to right, black calc(${pen}% - 6%), transparent calc(${pen}% + 2%))`;
useEffect(() => {
if (!inView) return;
if (reduceMotion) {
pen.set(110);
return;
}
// a pen moves at a fairly even pace, easing only at the very start and end
const controls = animate(pen, 110, { duration, delay, ease: [0.37, 0, 0.63, 1] });
return () => controls.stop();
}, [inView, reduceMotion, duration, delay, pen]);
useEffect(() => {
const element = flourishRef.current;
if (!element) return;
const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
observer.observe(element);
return () => observer.disconnect();
}, []);
const signed = inView || !!reduceMotion;
return (
<Tag ref={ref} className={`relative inline-block ${className}`}>
<motion.span
className="inline-block whitespace-nowrap px-[0.1em] leading-[1.3]"
style={{ fontFamily, color, maskImage: mask, WebkitMaskImage: mask }}
>
{text}
</motion.span>
{flourish && (
<span ref={flourishRef} aria-hidden className="pointer-events-none absolute inset-x-[8%] bottom-[0.02em] block h-[0.3em]">
{width > 0 && (
<svg width={width} height="100%" viewBox={`0 0 ${width} 20`} preserveAspectRatio="none" className="absolute inset-0 overflow-visible">
{/* the group hides the pen's round cap until the flourish begins */}
<motion.g
initial={{ opacity: reduceMotion ? 1 : 0 }}
animate={{ opacity: signed ? 1 : 0 }}
transition={{ duration: reduceMotion ? 0 : 0.15, delay: reduceMotion ? 0 : delay + duration * 0.9 }}
>
<motion.path
// a long tail that kicks up at the end, drawn after the last letter
d={`M 0 12 C ${width * 0.3} 20, ${width * 0.65} 2, ${width * 0.88} 10 S ${width * 0.98} 4, ${width} 1`}
fill="none"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
initial={{ pathLength: reduceMotion ? 1 : 0 }}
animate={{ pathLength: signed ? 1 : 0 }}
transition={{ duration: reduceMotion ? 0 : 0.6, delay: reduceMotion ? 0 : delay + duration * 0.9, ease: "easeOut" }}
/>
</motion.g>
</svg>
)}
</span>
)}
</Tag>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | What to write. |
| fontFamily | string | "cursive" | A script font you load, e.g. Mrs Saint Delafield. |
| color | string | "#F6F1E8" | Ink colour. |
| duration | number | 2.2 | Seconds to write the line. |
| delay | number | 0 | Seconds to wait once in view. |
| flourish | boolean | true | Draw a flourish underneath. |
| as | ElementType | "p" | Element to render. |
Monthly revenue
Number Ticker
Counts up to a value with an eased finish when it scrolls into view.
Small details make the whole thing feel alive.
Word Rise
Words lift out of a blur one after another as the line scrolls into view.
Split-Flap Display
An airport departures board: every tile flips through its drum, one character at a time, to land on your text.