Command Search
Search for a page
/ Components / Case Study Grid

Case Study Grid

Portfolio

Three-column portfolio grid with light/dark image variants, a category tag, shimmering titles, and dual case-study/preview CTA buttons per card.

portfoliocase-studiesgrid
Open preview
Live Preview
AI Generation Prompt

Create a Svelte 5 (runes) portfolio/case-study grid. Centered header (shimmering-text heading + subtext). Below, a 3-column responsive grid (1 col mobile, 2 col sm, 3 col lg) with generous vertical gaps between rows. Each item: a rounded, shadowed cover image (with an optional dark-mode variant swapped via `dark:hidden`/`dark:block`), a small pill-shaped category tag with a bolt icon, a shimmering-text title linking to a case-study page, a description paragraph, and a button row with an optional solid 'Case study' button and an optional outlined 'Live preview' button (with an external-link icon) — both wrapped in a subtle looping shine/sheen hover effect, and both conditionally rendered only if their href is provided. Stagger each card's scroll-reveal entrance by 100ms × index. Data-driven via `content: { heading, subtext, items: {image, image_dark?, image_alt, tag, title, description, case_study_href?, preview_href?}[] }`.

<script lang="ts">
	import ShimmeringText from '$lib/components/animate-ui/ShimmeringText.svelte';
	import Shine from '$lib/components/animate-ui/Shine.svelte';
	import { reveal } from '$lib/actions/reveal';

	type PortfolioItem = {
		image: string;
		image_dark?: string;
		image_alt: string;
		tag: string;
		title: string;
		description: string;
		case_study_href?: string;
		preview_href?: string;
	};
	type Props = { content: { heading: string; subtext: string; items: PortfolioItem[] } };
	let { content }: Props = $props();
</script>

<section class="bg-background antialiased">
	<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" use:reveal>
			<h2 class="text-3xl font-extrabold leading-tight tracking-tight text-foreground sm:text-4xl">
				<ShimmeringText text={content.heading} />
			</h2>
			<p class="mt-4 text-base font-normal text-muted-foreground sm:text-xl">{content.subtext}</p>
		</div>

		<div class="grid grid-cols-1 gap-12 mt-12 sm:gap-8 lg:gap-16 sm:mt-16 sm:grid-cols-2 lg:grid-cols-3">
			{#each content.items as item, i}
				<div use:reveal={{ delay: i * 100 }}>
					{#if item.image_dark}
						<img class="object-cover w-full rounded-lg shadow-lg dark:hidden mb-6" src={item.image} alt={item.image_alt} loading="lazy" decoding="async" />
						<img class="hidden object-cover w-full rounded-lg shadow-lg dark:block mb-6" src={item.image_dark} alt={item.image_alt} loading="lazy" decoding="async" />
					{:else}
						<img class="object-cover w-full rounded-lg shadow-lg mb-6" src={item.image} alt={item.image_alt} loading="lazy" decoding="async" />
					{/if}

					<div class="space-y-3 mb-6">
						<span class="bg-primary/10 text-primary text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded">
							<svg aria-hidden="true" class="w-3 h-3 mr-1" viewBox="0 0 20 20" fill="currentColor">
								<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
							</svg>
							{item.tag}
						</span>

						<h3 class="text-2xl font-bold leading-tight text-foreground">
							<a href={item.case_study_href ?? '#'} class="hover:underline">
								<ShimmeringText text={item.title} />
							</a>
						</h3>

						<p class="text-lg font-normal text-muted-foreground">{item.description}</p>
					</div>

					<div class="flex items-center gap-4">
						{#if item.case_study_href}
							<Shine loop loopDelay={3000} delay={i * 300} color="#ffffff" opacity={0.4} class="rounded-lg">
								<a
									href={item.case_study_href}
									class="block bg-primary text-primary-foreground hover:bg-primary/90 font-medium rounded-lg text-sm px-5 py-2.5 transition-colors"
								>
									Case study
								</a>
							</Shine>
						{/if}
						{#if item.preview_href}
							<Shine loop loopDelay={3000} delay={i * 300 + 200} color="#0078B7" opacity={0.3} class="rounded-lg">
								<a
									href={item.preview_href}
									class="inline-flex items-center justify-center px-5 py-2.5 text-sm font-medium text-foreground bg-background border border-border rounded-lg shrink-0 hover:bg-accent hover:text-accent-foreground transition-colors"
								>
									<svg aria-hidden="true" class="w-5 h-5 mr-2 -ml-1" viewBox="0 0 20 20" fill="currentColor">
										<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
										<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
									</svg>
									Live preview
								</a>
							</Shine>
						{/if}
					</div>
				</div>
			{/each}
		</div>
	</div>
</section>