Image Carousel Hero
HeroHero section with a 3D rotating image carousel, mouse parallax tilt, and a features grid. Theme-aware, fully animated.
Create a hero section component with a 3D rotating image carousel. Layout (top to bottom, all vertically centered in min-h-screen): 1. Circular image carousel — 8 portrait photos (w-32 h-40 on mobile, w-40 h-48 on sm+) positioned in a circular orbit (radius 180px) using cos/sin math. Angles rotate 0.5° every 50ms via setInterval. Each card has an individual rotateZ tilt. The whole ring tilts toward the mouse cursor via rotateX/rotateY (±10° parallax from mouse position relative to container). Cards: rounded-2xl, overflow-hidden, shadow-2xl, hover:scale-110. Shimmer gradient overlay (from-white/20) appears on hover. Container: h-96 (sm:h-[500px]), perspective: 1000px, transform-style: preserve-3d. 2. Centered heading — font-serif, text-4xl/5xl/6xl, font-bold, text-balance, leading-tight. 3. Subtext — text-lg/xl, text-muted-foreground. 4. CTA button — rounded-full, px-8 py-3, bg-primary, with ArrowRight icon that slides right on hover (group-hover:translate-x-1). 5. Features grid — 1 col mobile → 3 cols sm+. Cards: text-center p-6 rounded-xl bg-card/50 backdrop-blur-sm border border-border/50, hover:bg-card/80, h3 group-hover:text-primary. Background: Two bg-primary/5 gradient blobs (top-right and bottom-left) with blur-3xl animate-pulse. All colors use CSS custom properties for dark mode support.
<script lang="ts">
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
import { Button } from '$lib/components/ui/button';
type Feature = { title: string; description: string };
type ImageItem = { id: string; src: string; alt: string; rotation: number };
type Props = {
content: {
heading: string;
subtext: string;
cta_label: string;
images: ImageItem[];
features: Feature[];
};
};
let { content }: Props = $props();
let angles: number[] = $state([]);
let mouseX = $state(0.5);
let mouseY = $state(0.5);
$effect(() => {
const count = content.images.length;
angles = Array.from({ length: count }, (_, i) => i * (360 / count));
const id = setInterval(() => {
angles = angles.map((a) => (a + 0.5) % 360);
}, 50);
return () => clearInterval(id);
});
function handleMouseMove(e: MouseEvent) {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = (e.clientY - rect.top) / rect.height;
}
</script>
<div class="relative w-full min-h-screen bg-linear-to-b from-background via-background to-background overflow-hidden">
<!-- Ambient blobs -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-0 right-0 w-96 h-96 bg-linear-to-br from-primary/5 to-transparent rounded-full blur-3xl animate-pulse"></div>
<div class="absolute bottom-0 left-0 w-96 h-96 bg-linear-to-tr from-primary/5 to-transparent rounded-full blur-3xl animate-pulse"></div>
</div>
<div class="relative flex flex-col items-center justify-center min-h-screen px-4 sm:px-6 lg:px-8 py-12">
<!-- Rotating image carousel -->
<div
class="relative w-full max-w-6xl h-96 sm:h-[500px] mb-12 sm:mb-16"
onmousemove={handleMouseMove}
onmouseleave={() => { mouseX = 0.5; mouseY = 0.5; }}
role="img"
aria-label="Rotating AI-generated image gallery"
>
<div class="absolute inset-0 flex items-center justify-center" style="perspective: 1000px">
{#each content.images as img, i}
{@const angle = (angles[i] ?? 0) * (Math.PI / 180)}
{@const x = Math.cos(angle) * 180}
{@const y = Math.sin(angle) * 180}
{@const rx = (mouseY - 0.5) * 20}
{@const ry = (mouseX - 0.5) * 20}
<div
class="absolute w-32 h-40 sm:w-40 sm:h-48 transition-all duration-300"
style="transform: translate({x}px, {y}px) rotateX({rx}deg) rotateY({ry}deg) rotateZ({img.rotation}deg); transform-style: preserve-3d;"
>
<div
class="relative w-full h-full rounded-2xl overflow-hidden shadow-2xl transition-all duration-300 hover:scale-110 cursor-pointer group"
style="transform-style: preserve-3d;"
>
<img
src={img.src}
alt={img.alt}
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
loading={i < 3 ? 'eager' : 'lazy'}
/>
<div class="absolute inset-0 bg-linear-to-br from-white/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
</div>
{/each}
</div>
</div>
<!-- Heading + CTA -->
<div class="text-center max-w-2xl mx-auto mb-8">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-serif font-bold mb-4 sm:mb-6 text-balance leading-tight">
{content.heading}
</h1>
<p class="text-lg sm:text-xl text-muted-foreground mb-8 text-balance">
{content.subtext}
</p>
<Button class="inline-flex items-center gap-2 px-8 py-3 rounded-full font-medium hover:shadow-lg hover:scale-105 transition-all duration-300 active:scale-95 group">
{content.cta_label}
<ArrowRightIcon class="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Button>
</div>
<!-- Features grid -->
<div class="w-full max-w-4xl grid grid-cols-1 sm:grid-cols-3 gap-6 sm:gap-8 mt-8">
{#each content.features as feature}
<div class="text-center p-6 rounded-xl bg-card/50 backdrop-blur-sm border border-border/50 hover:bg-card/80 hover:border-border transition-all duration-300 group">
<h3 class="text-lg sm:text-xl font-semibold mb-2 group-hover:text-primary transition-colors">
{feature.title}
</h3>
<p class="text-sm sm:text-base text-muted-foreground">{feature.description}</p>
</div>
{/each}
</div>
</div>
</div>