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.
An editorial carousel for type, product, or archive specimens with numbered tabs and hard snap motion.
MSource LandMellow UIWhy it stands out
An editorial carousel for type, product, or archive specimens with numbered tabs and hard snap motion.
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/specimen-carousel-demo.tsx
File content: "use client";
import React from "react";
import { SpecimenCarousel } from "../mellow/specimen-carousel";
const specimens = [
{
kicker: "Typeface / 1896",
title: "Mercury",
sample: "Mm",
meta: "Display italic",
description:
"High-contrast forms for loud editorial openers, tuned with tight tracking and a heavy folio presence.",
},
{
kicker: "Archive / Plate 12",
title: "Basalt",
sample: "Bb",
meta: "Industrial serif",
description:
"Blocky, mineral weight with registration marks and a mechanical slide between specimens.",
},
{
kicker: "Foundry / Proof",
title: "Orchid",
sample: "Oo",
meta: "Soft grotesk",
description:
"A warmer card for brand systems that need some softness without losing the print-room character.",
},
{
kicker: "Index / New",
title: "Vellum",
sample: "Vv",
meta: "Text family",
description:
"Quiet bookish texture for long-form pages, product notes, and restrained launch narratives.",
},
];
export default function SpecimenCarouselDemo() {
return (
<div className="w-full p-2 sm:p-4">
<SpecimenCarousel items={specimens} className="w-full" />
</div>
);
}
File location: components/mellow/specimen-carousel.tsx
File content: "use client";
import React, { useEffect, useMemo, useState } from "react";
export interface SpecimenItem {
title: string;
kicker?: string;
description?: string;
meta?: string;
sample?: string;
}
export interface SpecimenCarouselProps {
items?: SpecimenItem[];
initialIndex?: number;
autoPlay?: boolean;
interval?: number;
className?: string;
style?: React.CSSProperties;
onIndexChange?: (index: number) => void;
}
const DEFAULT_ITEMS: SpecimenItem[] = [
{
kicker: "Specimen 01",
title: "Editorial",
sample: "Aa",
meta: "72 pt / italic",
description:
"A slow snap carousel for type samples, product stories, and image-led editorial modules.",
},
{
kicker: "Specimen 02",
title: "Mechanical",
sample: "Rr",
meta: "Mono rail",
description:
"Numbered tabs, registration marks, and a hard-set stage keep the motion tactile.",
},
{
kicker: "Specimen 03",
title: "Archive",
sample: "Gg",
meta: "Folio set",
description:
"Each panel behaves like a printed card sliding under a loupe, not a generic carousel.",
},
];
function clampIndex(index: number, length: number) {
if (length === 0) return 0;
return ((index % length) + length) % length;
}
function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReduced(media.matches);
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
return reduced;
}
export function SpecimenCarousel({
items = DEFAULT_ITEMS,
initialIndex = 0,
autoPlay = false,
interval = 4200,
className,
style,
onIndexChange,
}: SpecimenCarouselProps) {
const reduced = usePrefersReducedMotion();
const safeItems = items.length > 0 ? items : DEFAULT_ITEMS;
const [index, setIndex] = useState(() => clampIndex(initialIndex, safeItems.length));
const active = safeItems[index];
const folio = useMemo(() => String(index + 1).padStart(2, "0"), [index]);
const goTo = (nextIndex: number) => {
const next = clampIndex(nextIndex, safeItems.length);
setIndex(next);
onIndexChange?.(next);
};
useEffect(() => {
setIndex((current) => clampIndex(current, safeItems.length));
}, [safeItems.length]);
useEffect(() => {
if (!autoPlay || reduced || safeItems.length < 2) return;
const id = window.setInterval(() => {
setIndex((current) => {
const next = clampIndex(current + 1, safeItems.length);
onIndexChange?.(next);
return next;
});
}, interval);
return () => window.clearInterval(id);
}, [autoPlay, interval, onIndexChange, reduced, safeItems.length]);
return (
<section
className={[
"relative overflow-hidden border border-(--rule) bg-(--background) text-(--ink)",
className,
]
.filter(Boolean)
.join(" ")}
style={style}
aria-roledescription="carousel"
aria-label="Specimen carousel"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "ArrowRight") goTo(index + 1);
if (event.key === "ArrowLeft") goTo(index - 1);
}}
>
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(90deg,rgba(var(--ink-rgb),0.055)_1px,transparent_1px),linear-gradient(0deg,rgba(var(--ink-rgb),0.04)_1px,transparent_1px)] bg-size-[48px_48px]" />
<div className="relative grid min-h-[268px] grid-cols-1 sm:min-h-[420px] md:grid-cols-[7rem_1fr]">
<aside className="hidden border-r border-(--rule) md:flex md:flex-col md:justify-between">
<div className="p-4 font-mono text-[0.65rem] uppercase tracking-[0.18em] text-[rgba(var(--ink-rgb),0.45)] [writing-mode:vertical-rl]">
specimen index
</div>
<div className="border-t border-(--rule) p-4 font-mono text-3xl tracking-[-0.12em]">
{folio}
</div>
</aside>
<div className="relative flex min-h-[268px] flex-col justify-between p-3 sm:min-h-[420px] sm:p-7">
<div className="flex items-start justify-between gap-4">
<div>
<p className="font-mono text-[0.68rem] uppercase tracking-[0.22em] text-[rgba(var(--ink-rgb),0.5)]">
{active.kicker ?? `Specimen ${folio}`}
</p>
<p className="mt-2 font-mono text-[0.68rem] uppercase tracking-[0.18em] text-[rgba(var(--ink-rgb),0.34)]">
{active.meta ?? "Folio carousel"}
</p>
</div>
<div className="flex border border-(--rule) bg-[rgba(var(--background-rgb),0.72)]">
<button
type="button"
onClick={() => goTo(index - 1)}
className="h-9 w-9 cursor-pointer border-r border-(--rule) font-mono text-sm sm:h-10 sm:w-10 text-(--ink) transition-colors hover:bg-[rgba(var(--ink-rgb),0.08)]"
aria-label="Previous specimen"
>
←
</button>
<button
type="button"
onClick={() => goTo(index + 1)}
className="h-9 w-9 cursor-pointer font-mono text-sm sm:h-10 sm:w-10 text-(--ink) transition-colors hover:bg-[rgba(var(--ink-rgb),0.08)]"
aria-label="Next specimen"
>
→
</button>
</div>
</div>
<div className="grid items-end gap-3 py-2 sm:gap-6 sm:py-10 md:grid-cols-[1fr_13rem]">
<div>
<div className="relative mb-3 inline-flex sm:mb-5">
<span className="absolute -left-3 -top-3 h-3 w-3 border-l border-t border-(--ink)" />
<span className="absolute -right-3 -bottom-3 h-3 w-3 border-b border-r border-(--ink)" />
<span className="font-serif text-[clamp(3.75rem,20vw,13rem)] italic leading-[0.72] tracking-[-0.12em] text-(--ink)">
{active.sample ?? active.title.slice(0, 2)}
</span>
</div>
<h3 className="font-serif text-[clamp(2.25rem,8vw,6rem)] italic leading-[0.86] tracking-[-0.075em]">
{active.title}
</h3>
</div>
<p className="max-w-56 text-[0.8125rem] leading-5 text-[rgba(var(--ink-rgb),0.58)] sm:text-sm sm:leading-6">
{active.description ??
"Use this panel for a type specimen, featured artifact, product detail, or editorial story."}
</p>
</div>
<div className="overflow-hidden border-t border-(--rule) pt-2 sm:pt-4">
<div
className="flex gap-3 transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
style={{ transform: `translateX(${Math.max(0, index - 1) * -8.75}rem)` }}
>
{safeItems.map((item, itemIndex) => {
const activeItem = itemIndex === index;
return (
<button
key={`${item.title}-${itemIndex}`}
type="button"
onClick={() => goTo(itemIndex)}
className={[
"min-w-28 cursor-pointer border p-2.5 text-left transition-all duration-300 sm:min-w-32 sm:p-3",
activeItem
? "border-(--ink) bg-(--ink) text-(--background)"
: "border-(--rule) bg-[rgba(var(--background-rgb),0.66)] text-(--ink) hover:border-[rgba(var(--ink-rgb),0.42)]",
].join(" ")}
aria-label={`Show ${item.title}`}
aria-current={activeItem ? "true" : undefined}
>
<span className="block font-mono text-[0.62rem] uppercase tracking-[0.18em] opacity-60">
{String(itemIndex + 1).padStart(2, "0")}
</span>
<span className="mt-2 block truncate font-serif text-lg italic sm:mt-4 sm:text-xl tracking-[-0.04em]">
{item.title}
</span>
</button>
);
})}
</div>
</div>
</div>
</div>
</section>
);
}
export default SpecimenCarousel;
Usage notes:
Add a SpecimenCarousel component from the mellow library. It renders an editorial carousel with a large specimen stage, numbered tab strip, keyboard arrow navigation, and optional autoplay. Pass items with title, kicker, sample, meta, and description. Key props: items, initialIndex, autoPlay, interval, onIndexChange.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI