Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.
A typographic marquee whose drift speed and skew react to scroll velocity — rows alternate direction and lean into hard scrolls.
MSource LandMellow UIWhy it stands out
A typographic marquee whose drift speed and skew react to scroll velocity — rows alternate direction and lean into hard scrolls.
You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11
If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.
Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.
Copy-paste these files:
File location: components/velocity-band-demo.tsx
File content: "use client";
import React from "react";
import { VelocityBand } from "../mellow/velocity-band";
export default function VelocityBandDemo() {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-5 py-6 sm:gap-6 sm:py-10">
<VelocityBand
className="w-full border-y border-(--rule) py-6"
rows={[
["Copy", "Paste", "Own", "Texture", "Timing", "Theatre"],
["Motion", "Editorial", "Canvas", "Serif", "Mono", "Ink"],
]}
/>
<p className="px-5 text-center font-mono text-[0.625rem] tracking-[0.24em] text-[rgba(var(--ink-rgb),0.35)] uppercase">
Scroll the page — the band leans into your velocity
</p>
</div>
);
}
File location: components/mellow/velocity-band.tsx
File content: "use client";
import React, { useEffect, useRef } from "react";
export interface VelocityBandProps {
/** One array of words per marquee row. Rows alternate direction. */
rows?: string[][];
/** Base drift speed in px/s when the page is still. */
baseSpeed?: number;
/** How much scroll velocity boosts the drift (0 disables the reaction). */
velocityBoost?: number;
/** Maximum skew angle (deg) applied at high scroll velocity. */
maxSkew?: number;
/** Alternate serif-italic / sans words. Set false for all-sans. */
serifAlternate?: boolean;
/** Read scroll velocity from this element instead of the window. */
containerRef?: React.RefObject<HTMLElement | null>;
className?: string;
style?: React.CSSProperties;
}
function MarqueeRow({
words,
serifAlternate,
rowRef,
}: {
words: string[];
serifAlternate: boolean;
rowRef: (el: HTMLDivElement | null) => void;
}) {
const items = [...words, ...words, ...words, ...words];
return (
<div className="flex overflow-hidden whitespace-nowrap">
<div ref={rowRef} className="flex shrink-0 items-baseline will-change-transform">
{items.map((word, i) => (
<span key={i} className="flex shrink-0 items-baseline">
<span
className={
serifAlternate && i % 2 === 0
? "font-serif text-[clamp(2.75rem,7vw,6rem)] leading-[1.1] text-(--ink) italic"
: "font-sans text-[clamp(2.75rem,7vw,6rem)] leading-[1.1] font-medium tracking-[-0.04em] text-(--ink)"
}
>
{word}
</span>
<span
aria-hidden="true"
className="mx-[clamp(1.25rem,3vw,2.75rem)] inline-block h-[6px] w-[6px] translate-y-[-0.5em] rounded-full bg-[rgba(var(--ink-rgb),0.25)]"
/>
</span>
))}
</div>
</div>
);
}
/**
* A typographic marquee whose drift speed and skew react to scroll velocity.
* Rows alternate direction; scroll hard and the band leans into the motion.
*/
export function VelocityBand({
rows = [
["Copy", "Paste", "Own", "Texture", "Timing", "Theatre"],
["Motion", "Editorial", "Canvas", "Serif", "Mono", "Ink"],
],
baseSpeed = 54,
velocityBoost = 200,
maxSkew = 6,
serifAlternate = true,
containerRef,
className,
style,
}: VelocityBandProps) {
const sectionRef = useRef<HTMLDivElement>(null);
const rowRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const section = sectionRef.current;
const els = rowRefs.current.filter(Boolean) as HTMLDivElement[];
if (!section || els.length === 0) return;
const reduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (reduced) return;
const readScroll = () =>
containerRef?.current ? containerRef.current.scrollTop : window.scrollY;
let raf = 0;
let running = false;
const offsets = els.map(() => 0);
let lastScroll = readScroll();
let velocity = 0;
let skew = 0;
let lastTime = performance.now();
const frame = (now: number) => {
if (!running) return;
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
const sy = readScroll();
const instV = (sy - lastScroll) / Math.max(dt, 0.001);
lastScroll = sy;
velocity += (instV - velocity) * 0.12;
const boost = Math.min(Math.abs(velocity) / 900, 3);
const speed = (baseSpeed + boost * velocityBoost) * dt;
const targetSkew = Math.max(
-maxSkew,
Math.min(maxSkew, velocity / 260)
);
skew += (targetSkew - skew) * 0.1;
els.forEach((el, i) => {
const dir = i % 2 === 0 ? -1 : 1;
const half = el.scrollWidth / 2;
offsets[i] += speed * dir;
if (offsets[i] <= -half) offsets[i] += half;
if (offsets[i] >= 0) offsets[i] -= half;
el.style.transform = `translate3d(${offsets[i]}px,0,0) skewX(${-skew}deg)`;
});
raf = requestAnimationFrame(frame);
};
const start = () => {
if (running) return;
running = true;
lastTime = performance.now();
lastScroll = readScroll();
raf = requestAnimationFrame(frame);
};
const stop = () => {
running = false;
cancelAnimationFrame(raf);
};
const io = new IntersectionObserver(
([entry]) => (entry.isIntersecting ? start() : stop()),
{ threshold: 0 }
);
io.observe(section);
const onVisibility = () => {
if (document.hidden) stop();
else if (section.getBoundingClientRect().bottom > 0) start();
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
stop();
io.disconnect();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [baseSpeed, velocityBoost, maxSkew, rows.length, containerRef]);
return (
<div
ref={sectionRef}
aria-hidden="true"
className={["flex flex-col gap-2 overflow-hidden", className]
.filter(Boolean)
.join(" ")}
style={style}
>
{rows.map((words, i) => (
<div key={i} className={i % 2 === 1 ? "opacity-40" : undefined}>
<MarqueeRow
words={words}
serifAlternate={serifAlternate}
rowRef={(el) => {
rowRefs.current[i] = el;
}}
/>
</div>
))}
</div>
);
}
export default VelocityBand;
Usage notes:
Add a VelocityBand component from the mellow library. It renders rows of large typographic marquee text that drift continuously; scroll velocity boosts the drift speed and skews the rows so the band leans into the motion. Rows alternate direction and odd rows are dimmed. Props: rows (string[][], one array of words per row), baseSpeed (px/s, default 54), velocityBoost (default 200), maxSkew (deg, default 6), serifAlternate (alternate serif-italic/sans words, default true). Respects prefers-reduced-motion by rendering static rows.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI