Command Search
Search for a page
/ Components / Leadership Carousel

Leadership Carousel

Team

Overlapping image/bio carousel for leadership profiles — large photo with a card sliding over its edge on desktop, a stacked layout on mobile, dot navigation and prev/next arrows.

teamcarouselleadershipbio
Open preview
Live Preview
AI Generation Prompt

Create a Svelte 5 (runes) leadership/board-member carousel using an embla-based Carousel primitive (loop enabled, centered alignment). Centered header (shimmering-text heading, optional subtext, optional 'meet the full team' link). Each slide, on desktop (`md:flex`, hidden on mobile): a large square photo (470×470) on the left with `object-cover object-top`, and a card overlapping its right edge with a large negative left margin, containing the member's name, title, a description paragraph, and a row of circular social icon buttons (dark circle background, brand SVG icon, opens in new tab). On mobile (`md:hidden`): a stacked, centered layout with a square photo, then the same text content below, without the overlap trick. Navigation: round prev/next arrow buttons on either side of a row of small dot indicators (dots reflect the carousel API's `selectedScrollSnap()`, updated via an `$effect` that subscribes to the carousel's `select` event). Data-driven via `content: { heading, subtext?, members_href?, members: {name, title, description, image, social_links: {icon, url, label}[]}[] }`.

<script lang="ts">
	import type { CarouselAPI } from '$lib/components/ui/carousel/context.js';
	import * as Carousel from '$lib/components/ui/carousel/index.js';
	import ShimmeringText from '$lib/components/animate-ui/ShimmeringText.svelte';
	import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
	import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
	import { reveal } from '$lib/actions/reveal';

	type SocialLink = { icon: string; url: string; label: string };
	type BoardMember = { name: string; title: string; description: string; image: string; social_links: SocialLink[] };
	type Props = { content: { heading: string; subtext?: string; members_href?: string; members: BoardMember[] } };
	let { content }: Props = $props();

	let api = $state<CarouselAPI | undefined>(undefined);
	let selectedIndex = $state(0);

	$effect(() => {
		if (!api) return;
		selectedIndex = api.selectedScrollSnap();
		api.on('select', () => {
			selectedIndex = api!.selectedScrollSnap();
		});
	});

	const socialIcons: Record<string, string> = {
		facebook: `<path fill-rule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clip-rule="evenodd"/>`,
		linkedin: `<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>`,
		twitter: `<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/>`,
		x: `<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>`,
		github: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>`,
		youtube: `<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>`,
	};
</script>

<section class="relative overflow-hidden">
	<div class="max-w-screen-xl px-4 py-8 mx-auto lg:px-6 sm:py-16 lg:py-24">
		<div class="max-w-2xl mx-auto text-center mb-12 sm:mb-16" use:reveal>
			<h2 class="text-3xl font-extrabold leading-tight tracking-tight text-foreground sm:text-4xl">
				<ShimmeringText text={content.heading} />
			</h2>
			{#if content.subtext}
				<p class="mt-4 text-base font-normal text-muted-foreground sm:text-xl">{content.subtext}</p>
			{/if}
			{#if content.members_href}
				<a
					href={content.members_href}
					class="inline-flex items-center gap-1.5 mt-6 text-sm font-medium text-primary hover:underline"
				>
					Meet the full team
					<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
						<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
					</svg>
				</a>
			{/if}
		</div>

		<div use:reveal={{ animation: 'fade', delay: 200 }}>
		<Carousel.Root
			opts={{ loop: true, align: 'center' }}
			setApi={(emblaApi) => (api = emblaApi)}
			class="w-full max-w-5xl mx-auto"
		>
			<Carousel.Content class="-ms-0 will-change-transform">
				{#each content.members as member}
					<Carousel.Item class="ps-0">
						<!-- Desktop -->
						<div class="hidden md:flex relative items-center">
							<div class="w-[470px] h-[470px] rounded-3xl overflow-hidden bg-muted flex-shrink-0 backface-hidden">
								<img
									src={member.image}
									alt={member.name}
									class="w-full h-full object-cover object-top"
									loading="lazy"
									decoding="async"
									width="470"
									height="470"
								/>
							</div>
							<div class="bg-card rounded-3xl shadow-lg p-8 ml-[-80px] z-10 max-w-xl flex-1 backface-hidden">
								<div class="mb-6">
									<h3 class="text-2xl font-bold text-foreground mb-2">{member.name}</h3>
									<p class="text-sm font-medium text-muted-foreground">{member.title}</p>
								</div>
								<p class="text-foreground text-base leading-relaxed mb-8">{member.description}</p>
								<div class="flex space-x-4">
									{#each member.social_links as social}
										{#if socialIcons[social.icon]}
											<a
												href={social.url || '#'}
												target="_blank"
												rel="noopener noreferrer"
												class="w-12 h-12 bg-foreground rounded-full flex items-center justify-center hover:opacity-80 hover:scale-105 transition-all cursor-pointer"
												aria-label={social.label}
											>
												<svg class="w-5 h-5 text-background" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
													<!-- eslint-disable-next-line svelte/no-at-html-tags -->
													{@html socialIcons[social.icon]}
												</svg>
											</a>
										{/if}
									{/each}
								</div>
							</div>
						</div>

						<!-- Mobile -->
						<div class="md:hidden max-w-sm mx-auto text-center">
							<div class="w-full aspect-square bg-muted rounded-3xl overflow-hidden mb-6">
								<img
									src={member.image}
									alt={member.name}
									class="w-full h-full object-cover object-top"
									loading="lazy"
									decoding="async"
								/>
							</div>
							<div class="px-4">
								<h3 class="text-xl font-bold text-foreground mb-2">{member.name}</h3>
								<p class="text-sm font-medium text-muted-foreground mb-4">{member.title}</p>
								<p class="text-foreground text-sm leading-relaxed mb-6">{member.description}</p>
								<div class="flex justify-center space-x-4">
									{#each member.social_links as social}
										{#if socialIcons[social.icon]}
											<a
												href={social.url || '#'}
												target="_blank"
												rel="noopener noreferrer"
												class="w-12 h-12 bg-foreground rounded-full flex items-center justify-center hover:opacity-80 transition-all"
												aria-label={social.label}
											>
												<svg class="w-5 h-5 text-background" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
													<!-- eslint-disable-next-line svelte/no-at-html-tags -->
													{@html socialIcons[social.icon]}
												</svg>
											</a>
										{/if}
									{/each}
								</div>
							</div>
						</div>
					</Carousel.Item>
				{/each}
			</Carousel.Content>

			<!-- Navigation -->
			<div class="flex justify-center items-center gap-6 mt-8">
				<button
					onclick={() => api?.scrollPrev()}
					aria-label="Previous member"
					class="w-12 h-12 rounded-full bg-muted border border-border shadow-md flex items-center justify-center hover:bg-accent transition-colors cursor-pointer"
				>
					<ChevronLeftIcon class="w-6 h-6 text-foreground" />
				</button>
				<div class="flex gap-2">
					{#each content.members as _, i}
						<button
							onclick={() => api?.scrollTo(i)}
							class="w-3 h-3 rounded-full transition-colors cursor-pointer {i === selectedIndex ? 'bg-foreground' : 'bg-muted-foreground/40'}"
							aria-label="Go to member {i + 1}"
						></button>
					{/each}
				</div>
				<button
					onclick={() => api?.scrollNext()}
					aria-label="Next member"
					class="w-12 h-12 rounded-full bg-muted border border-border shadow-md flex items-center justify-center hover:bg-accent transition-colors cursor-pointer"
				>
					<ChevronRightIcon class="w-6 h-6 text-foreground" />
				</button>
			</div>
		</Carousel.Root>
		</div>
	</div>
</section>