Product hero - cinematic orbit
kalunza-product-hero-cinematic-orbit.tsx
componentskill: emil-design-eng + design-taste-frontend + scroll-world + gsap + motion-design + threejs-r3fheroecommerceproductcinematicluxurydark
Brief
The prompt that produced this. Re-run it to generate more in the same taste.
- Aesthetic
- cinematic craft atlas - nocturnal, tactile, sculptural, asymmetric, grounded, luminous, African-modern
- Reference
- Kalunza passport-holder storyboard and multi-angle product photography, 2026-07-24
- Intent
- make one travel object feel like a world worth entering while keeping the product easy to understand and buy
- Guardrails
- always keep the product recognisable, use one muted emerald accent, hold a single dark theme, keep the primary action legible, collapse cleanly at 375pxnever use AI-purple glow, use beige-and-brass luxury defaults, add scroll instructions, use em dashes, animate without a storytelling or feedback purpose
Install
pnpm dlx shadcn@latest add http://localhost:3040/r/kalunza-product-hero-cinematic-orbit.json
Source
Meta block and library type import stripped - this is what pastes cleanly elsewhere.
'use client'
import { useState } from 'react'
const defaultImage =
'https://cdn.shopify.com/s/files/1/1026/5893/3104/files/FLASH1_673ff8c9-ef62-474d-8e7d-ad41cbaaff61.jpg?v=1783655598'
type ProductHeroProps = {
brand?: string
imageUrl?: string
headline?: string
body?: string
shopHref?: string
}
const chapters = ['Journey', 'Hands', 'Pattern', 'Inside', 'Piece']
export default function KalunzaProductHeroCinematicOrbit({
brand = 'Kalunza',
imageUrl = defaultImage,
headline = 'Made to move with you.',
body = 'A travel companion shaped by Soweto craft and a pattern that refuses to disappear.',
shopHref = 'https://kalunza.com/collections/all',
}: ProductHeroProps) {
const [activeChapter, setActiveChapter] = useState(0)
return (
<main id="top" className="relative min-h-[100dvh] overflow-hidden bg-[#080b09] text-[#f1f4ee]">
<img
src={imageUrl}
alt="Patterned travel accessory"
width={1600}
height={1200}
loading="eager"
fetchPriority="high"
className="absolute inset-0 size-full object-cover object-[68%_50%]"
/>
<div
aria-hidden
className="absolute inset-0 bg-[linear-gradient(90deg,#080b09_0%,rgba(8,11,9,.94)_37%,rgba(8,11,9,.42)_67%,rgba(8,11,9,.08)_100%),linear-gradient(0deg,rgba(4,6,5,.36),transparent_38%)]"
/>
<header className="relative z-20 mx-auto flex min-h-18 max-w-[1600px] items-center justify-between gap-5 px-5 py-4 sm:px-8 lg:px-14">
<a
href="#top"
className="flex items-center gap-3 font-semibold text-sm uppercase tracking-[0.22em] transition-[transform,color] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97] motion-reduce:transform-none motion-reduce:transition-none"
>
<span aria-hidden className="h-7 w-[17px] rounded-full bg-[#78947a]" />
{brand}
</a>
<nav
aria-label="Product story"
className="hidden items-center gap-1 rounded-full border border-white/15 bg-[#0e1210]/80 p-1 backdrop-blur-xl md:flex"
>
{chapters.map((chapter, index) => (
<button
key={chapter}
type="button"
aria-pressed={activeChapter === index}
onClick={() => setActiveChapter(index)}
className={`rounded-full px-4 py-2 text-xs transition-[transform,background-color,color] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97] motion-reduce:transform-none motion-reduce:transition-none ${
activeChapter === index
? 'bg-[#c9d8c7] text-[#09100b]'
: 'text-[#aeb6ae] hover:text-[#f1f4ee]'
}`}
>
{chapter}
</button>
))}
</nav>
<a
href={shopHref}
className="hidden whitespace-nowrap rounded-full bg-[#dce9da] px-5 py-3 font-semibold text-[#08100b] text-sm transition-[transform,background-color] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-[#eef5ec] active:scale-[0.97] motion-reduce:transform-none motion-reduce:transition-none sm:inline-flex"
>
Shop the holder
</a>
</header>
<section className="relative z-10 flex min-h-[calc(100dvh-72px)] items-end px-5 pb-28 sm:px-8 md:items-center md:pb-10 lg:px-14">
<div className="w-full max-w-[530px]">
<h1 className="max-w-[10ch] text-balance font-medium text-[clamp(2.4rem,5.35vw,5.35rem)] leading-[0.98] tracking-[-0.055em]">
{headline}
</h1>
<p className="mt-5 max-w-[31ch] text-[#d1d7d0] text-[clamp(1rem,1.22vw,1.14rem)] leading-[1.56]">
{body}
</p>
<div className="mt-6 flex flex-wrap gap-2">
{['Made in South Africa', 'Small-batch craft'].map((item) => (
<span
key={item}
className="rounded-full border border-[#97b69b]/35 bg-[#12261d]/70 px-3.5 py-2 font-semibold text-[#dce6dc] text-xs backdrop-blur-lg"
>
{item}
</span>
))}
</div>
<a
href={shopHref}
className="mt-7 inline-flex whitespace-nowrap rounded-full bg-[#dce9da] px-6 py-3.5 font-semibold text-[#08100b] text-sm transition-[transform,background-color] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-[#eef5ec] active:scale-[0.97] motion-reduce:transform-none motion-reduce:transition-none sm:hidden"
>
Shop the holder
</a>
</div>
</section>
<div
role="group"
aria-label={`Chapter ${activeChapter + 1} of ${chapters.length}: ${chapters[activeChapter]}`}
className="absolute top-1/2 right-6 z-20 hidden -translate-y-1/2 flex-col items-center gap-4 lg:flex"
>
{chapters.map((chapter, index) => (
<button
key={chapter}
type="button"
aria-label={`Show ${chapter}`}
onClick={() => setActiveChapter(index)}
className="grid size-7 place-items-center rounded-full transition-transform duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97] motion-reduce:transform-none motion-reduce:transition-none"
>
<span
aria-hidden
className={`block rounded-full bg-[#78947a] transition-[width,height,opacity] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] ${
activeChapter === index ? 'size-3 opacity-100' : 'size-1.5 opacity-50'
}`}
/>
</button>
))}
</div>
</main>
)
}