Slideshow Hero
Auto-advancing full-bleed slideshow with Ken Burns zoom on the active slide, slide counter, dot pagination, prev/next arrow controls, pause-on-hover, and italic caption typography. Below the slideshow: circular avatar overlap, Cormorant italic name, optional credentials line, and frosted-glass link buttons over a dark backdrop.
Preview
Source
tsx
"use client";
import { useEffect, useRef, useState } from "react";
import {
Instagram,
Twitter,
Youtube,
Mail,
Globe,
ShoppingBag,
PlayCircle,
Linkedin,
Music2,
ArrowUpRight,
ChevronLeft,
ChevronRight,
Pause,
Play,
type LucideIcon,
} from "lucide-react";
import type { GalleryItem, LinkIcon, LinksInBioData } from "../types";
import { defaultData } from "../defaultData";
const ICONS: Record<LinkIcon, LucideIcon> = {
instagram: Instagram,
twitter: Twitter,
tiktok: Music2,
youtube: Youtube,
spotify: Music2,
apple: Music2,
linkedin: Linkedin,
email: Mail,
globe: Globe,
shop: ShoppingBag,
play: PlayCircle,
};
const SLIDE_MS = 5500;
export interface LinksInBioSlideshowHeroProps {
data?: LinksInBioData;
}
export default function LinksInBioSlideshowHero({
data = defaultData,
}: LinksInBioSlideshowHeroProps) {
const slides: GalleryItem[] =
data.gallery && data.gallery.length > 0
? data.gallery
: [{ image: data.heroImage ?? data.avatar, caption: data.name }];
const [active, setActive] = useState(0);
const [paused, setPaused] = useState(false);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (paused || slides.length < 2) return;
timerRef.current = setInterval(() => {
setActive((a) => (a + 1) % slides.length);
}, SLIDE_MS);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [paused, slides.length]);
const prev = () => setActive((a) => (a - 1 + slides.length) % slides.length);
const next = () => setActive((a) => (a + 1) % slides.length);
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400..700;1,400..700&family=Inter:wght@400;500;600;700&display=swap"
/>
<style>{`
@keyframes slideshowhero-kenburns {
from { transform: scale(1.02) translate3d(0,0,0); }
to { transform: scale(1.12) translate3d(-1.5%, -1%, 0); }
}
`}</style>
<section
className="min-h-screen w-full bg-[#0c0c0e] text-white"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{/* Slideshow */}
<div
className="relative h-[60vh] min-h-[420px] w-full overflow-hidden"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{slides.map((slide, i) => (
<div
key={i}
aria-hidden={i !== active}
className="absolute inset-0 transition-opacity duration-700"
style={{ opacity: i === active ? 1 : 0 }}
>
<img
src={slide.image}
alt={slide.caption ?? ""}
width={1600}
height={2000}
loading={i === 0 ? "eager" : "lazy"}
className="h-full w-full object-cover"
style={{
animation:
i === active && !paused
? `slideshowhero-kenburns ${SLIDE_MS + 1500}ms ease-out forwards`
: "none",
willChange: "transform",
}}
/>
</div>
))}
{/* Bottom gradient */}
<div
aria-hidden
className="absolute inset-0"
style={{
background:
"linear-gradient(180deg, rgba(12,12,14,0) 35%, rgba(12,12,14,0.95) 100%)",
}}
/>
{/* Slide counter & caption — top-left */}
<div className="absolute left-5 top-5 flex items-center gap-3 text-xs">
<span
className="rounded-full border border-white/30 bg-black/30 px-3 py-1 font-mono backdrop-blur-md"
>
{String(active + 1).padStart(2, "0")} / {String(slides.length).padStart(2, "0")}
</span>
{slides[active]?.caption && (
<span
className="text-[11px] font-semibold uppercase tracking-[0.3em] text-white/85"
>
{slides[active].caption}
</span>
)}
</div>
{/* Pause/Play */}
<button
type="button"
aria-label={paused ? "Play slideshow" : "Pause slideshow"}
onClick={() => setPaused((p) => !p)}
className="absolute right-5 top-5 flex h-9 w-9 items-center justify-center rounded-full border border-white/30 bg-black/30 text-white backdrop-blur-md hover:bg-black/50"
>
{paused ? <Play className="h-4 w-4" /> : <Pause className="h-4 w-4" />}
</button>
{/* Prev/Next arrows */}
<button
type="button"
aria-label="Previous slide"
onClick={prev}
className="absolute left-3 top-1/2 hidden h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/20 bg-black/25 text-white backdrop-blur-md transition-colors hover:bg-black/45 sm:flex"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
type="button"
aria-label="Next slide"
onClick={next}
className="absolute right-3 top-1/2 hidden h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/20 bg-black/25 text-white backdrop-blur-md transition-colors hover:bg-black/45 sm:flex"
>
<ChevronRight className="h-5 w-5" />
</button>
{/* Caption block bottom */}
{slides[active]?.meta && (
<p
className="absolute inset-x-5 bottom-24 text-center text-sm italic text-white/90 sm:bottom-28"
style={{ fontFamily: "'Cormorant Garamond', serif" }}
>
"{slides[active].meta}"
</p>
)}
{/* Dots */}
<div className="absolute inset-x-0 bottom-6 flex items-center justify-center gap-2">
{slides.map((_, i) => (
<button
key={i}
type="button"
aria-label={`Slide ${i + 1}`}
onClick={() => setActive(i)}
className="rounded-full transition-all"
style={{
width: i === active ? 22 : 8,
height: 8,
background:
i === active ? "#fff" : "rgba(255,255,255,0.45)",
}}
/>
))}
</div>
</div>
{/* Avatar overlap + identity */}
<div className="relative mx-auto -mt-14 w-full max-w-[460px] px-5 pb-12">
<div className="flex flex-col items-center text-center">
<img
src={data.avatar}
alt={data.name}
width={128}
height={128}
loading="eager"
className="h-28 w-28 rounded-full object-cover"
style={{
border: "5px solid #0c0c0e",
boxShadow: "0 14px 32px rgba(0,0,0,0.45)",
}}
/>
<h1
className="mt-4 leading-tight text-white"
style={{
fontFamily: "'Cormorant Garamond', serif",
fontStyle: "italic",
fontSize: "clamp(2.2rem, 6.4vw, 2.8rem)",
}}
>
{data.name}
{data.verified && (
<span className="ml-2 align-middle text-base not-italic text-[#c9a961]">✓</span>
)}
</h1>
{data.handle && (
<p className="mt-1 text-sm text-white/55">{data.handle}</p>
)}
{data.credentials && (
<p className="mt-1 text-[11px] uppercase tracking-[0.28em] text-[#c9a961]">
{data.credentials}
</p>
)}
{data.bio && (
<p className="mt-3 max-w-[400px] text-[15px] leading-relaxed text-white/75">
{data.bio}
</p>
)}
</div>
<div className="mt-7 space-y-2.5">
{data.links.map((link) => {
const Icon = link.icon ? ICONS[link.icon] : ArrowUpRight;
return (
<a
key={link.label}
href={link.href}
className="group flex items-center gap-3 rounded-2xl border border-white/15 bg-white/[0.04] px-4 py-3.5 backdrop-blur-md transition-all hover:border-white/30 hover:bg-white/[0.08]"
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/10">
<Icon className="h-4 w-4 text-white" strokeWidth={1.6} />
</span>
<span className="flex-1 truncate text-sm font-medium text-white">
{link.label}
</span>
{link.badge && (
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider"
style={{ background: "#c9a961", color: "#0c0c0e" }}
>
{link.badge}
</span>
)}
<ArrowUpRight className="h-4 w-4 shrink-0 text-white/50 transition-colors group-hover:text-white" />
</a>
);
})}
</div>
{data.socials && data.socials.length > 0 && (
<div className="mt-7 flex items-center justify-center gap-4 text-white/65">
{data.socials.map((s, i) => {
const Icon = ICONS[s.type] ?? Globe;
return (
<a
key={i}
href={s.href}
aria-label={s.type}
className="rounded-full border border-white/15 p-2 transition-colors hover:text-white"
>
<Icon className="h-4 w-4" strokeWidth={1.6} />
</a>
);
})}
</div>
)}
</div>
</section>
</>
);
} Claude Code Instructions
CLI Install
npx innovations add slideshow-heroWhere to use it
An editorial slideshow link-in-bio page. Hardcoded dark palette with gold accent. Loads Cormorant Garamond and Inter from Google Fonts.
Pass a typed 'data' prop (LinksInBioData from src/registry/links-in-bio/types.ts). Provide:
- gallery — array of { image, caption, meta } where caption is the slide label (e.g., 'Lisbon') and meta is the italic quote shown beneath
- credentials — short line beneath the name, rendered in gold uppercase tracking
In Astro:
import LinksInBioSlideshowHero from '../components/innovations/links-in-bio/slideshow-hero';
<LinksInBioSlideshowHero client:load data={myProfile} />
In Next.js:
import LinksInBioSlideshowHero from '@/components/innovations/links-in-bio/slideshow-hero';
Best for: photographers, models, lifestyle creators, hospitality brands, dancers — anyone whose visual portfolio is the product. The slideshow auto-advances every 5.5s; users can pause via the top-right button.