Command Search
Search for a page
/ Components / Split Hero with Video

Split Hero with Video

Hero

Two-column hero: badge + shimmering heading + CTAs on the left, a playable video thumbnail with duration overlay on the right.

herovideosplit-layout
Open preview
Live Preview
AI Generation Prompt

Create a Svelte 5 (runes) hero section with a 2-column layout on large screens (stacked on mobile). Left column: a small outlined badge with a medal icon, a large shimmering-text heading, subtext, and two CTA buttons (solid primary, and outline) each wrapped in a subtle looping shine/sheen hover effect. Right column: a video thumbnail with rounded corners and shadow; clicking the centered play button (a circle badge with a play icon) swaps the thumbnail for a lazy-loaded YouTube iframe using local `$state`. Overlay a gradient fade at the bottom of the thumbnail with a duration label + duration text in the corner. Use a scroll-reveal action sliding the left column in from the left and the right column in from the right with a stagger delay. Data-driven via `content: { badge, heading, subtext, primary_cta:{label,href}, secondary_cta:{label,href}, media:{thumbnail_url,thumbnail_alt,video_url,video_title,duration_label,duration} }`.

<script lang="ts">
	import { Button } from '$lib/components/ui/button';
	import { Badge } from '$lib/components/ui/badge';
	import ShimmeringText from '$lib/components/animate-ui/ShimmeringText.svelte';
	import Shine from '$lib/components/animate-ui/Shine.svelte';
	import PlayIcon from '@lucide/svelte/icons/play';
	import MedalIcon from '@lucide/svelte/icons/medal';
	import { reveal } from '$lib/actions/reveal';

	type Props = {
		content: {
			badge: string;
			heading: string;
			subtext: string;
			primary_cta: { label: string; href: string };
			secondary_cta: { label: string; href: string };
			media: {
				thumbnail_url: string;
				thumbnail_alt: string;
				video_url: string;
				video_title: string;
				duration_label: string;
				duration: string;
			};
		};
	};
	let { content }: Props = $props();

	let isPlaying = $state(false);
</script>

<section class="relative z-0 w-full py-12 md:py-16 lg:py-20">
	<div class="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
		<div class="grid items-center gap-8 lg:grid-cols-2 lg:gap-12">
			<div class="flex flex-col items-center gap-6 lg:items-start" use:reveal={{ animation: 'left' }}>
				<Badge variant="outline" class="h-auto font-semibold rounded-md text-muted-foreground gap-2 px-4 py-2">
					<MedalIcon class="size-4" aria-hidden="true" />
					{content.badge}
				</Badge>

				<div class="flex flex-col gap-4 text-center lg:text-start">
					<h1 class="text-4xl font-bold tracking-tight text-balance md:text-5xl">
						<ShimmeringText text={content.heading} />
					</h1>
					<p class="text-muted-foreground text-balance md:text-lg">{content.subtext}</p>
				</div>

				<div class="flex flex-wrap items-center justify-center gap-4 lg:justify-start">
					<Shine loop loopDelay={3000} delay={0} color="#ffffff" opacity={0.4} class="rounded-lg">
						<Button size="lg" class="h-10 px-8" href={content.primary_cta.href}>
							{content.primary_cta.label}
						</Button>
					</Shine>
					<Shine loop loopDelay={3000} delay={400} color="#0078B7" opacity={0.3} class="rounded-lg">
						<Button variant="outline" size="lg" class="h-10 px-8 bg-background" href={content.secondary_cta.href}>
							{content.secondary_cta.label}
						</Button>
					</Shine>
				</div>
			</div>

			<div class="group relative isolate overflow-hidden rounded-xl" use:reveal={{ animation: 'right', delay: 150 }}>
				<div class="overflow-hidden border-0 shadow-lg rounded-xl">
					<div class="relative w-full aspect-video">
						{#if isPlaying}
							<iframe
								src={content.media.video_url}
								title={content.media.video_title}
								allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
								allowfullscreen
								class="absolute inset-0 w-full h-full rounded-lg"
								loading="lazy"
							></iframe>
						{:else}
							<div class="relative w-full h-full">
								<img
								src={content.media.thumbnail_url}
								alt={content.media.thumbnail_alt}
								class="w-full h-full rounded-lg object-cover dark:brightness-95 dark:invert"
								fetchpriority="high"
								decoding="sync"
							/>
								<div class="absolute inset-0 bg-linear-to-t from-background/40 via-background/20 to-transparent"></div>
								<button
									onclick={() => (isPlaying = true)}
									class="absolute inset-0 flex w-full h-full items-center justify-center hover:opacity-90 transition-opacity"
									aria-label="Play demo video"
								>
									<div class="bg-background/90 flex size-14 items-center justify-center rounded-full shadow-lg hover:scale-105 transition-transform">
										<PlayIcon size={20} class="ms-1" />
									</div>
								</button>
								<div class="absolute start-4 end-4 bottom-4">
									<p class="text-foreground text-sm font-medium">{content.media.duration_label}</p>
									<p class="text-muted-foreground text-xs">{content.media.duration}</p>
								</div>
							</div>
						{/if}
					</div>
				</div>
			</div>
		</div>
	</div>
</section>