Case Study Carousel
Horizontal case-study carousel. Carousel Desktop (content + image card) is used on /, /work, and /about. Carousel Mobile (image-dominant card with title overlay and success-metric sticker) renders below the lg breakpoint on those surfaces and in the home hero's right column.
Carousel Desktop
Default layout (variant="full"). 960px-wide cards with content on the left and image on the right. Shown with mock data.
Carousel Mobile
Compact image-dominated cards (variant="hero"). Used as the mobile layout for the case-studies module and inside the home-page hero's right column. Keeps the same rotating border animation on the active card. On first load, hero variants render a static first slide until the client can position the looping track; if that mount step takes longer, the mobile shimmer treatment can cover the gap so the clone slide never flashes.

E-commerce Platform Redesign
Loading skeleton — desktop
Shimmer state that mirrors the desktop carousel's active card. Used by WorkCarousel as a fade-in overlay during category filter transitions so the carousel never flash-swaps.
Loading skeleton — mobile
Mirrors the mobile hero card — aspect-square shimmer plus dots and arrow-button placeholders below.
Filter-aware wrapper (WorkCarousel)
WorkCarousel is the client wrapper used on /work. It reads the ?category= URL param (written by CategoryFilters), filters the case studies client-side, and fades the skeleton overlay in for 500ms during the transition so new cards never flash-replace old ones. If a filter produces zero matches, it renders a centered empty-state message instead.
View full version
Props
caseStudies
CaseStudy[] (required)Array of case study data from Sanity CMS.
variant
"full" | "hero" (default "full")"full" is the responsive Case Studies module: Carousel Mobile below lg, Carousel Desktop at lg+. "hero" forces Carousel Mobile at all widths — used inside the home-page hero, wrapped in a lg:block parent so it only shows on desktop.
title
string (default: "Case Studies", ignored when forced to hero)Section heading. Set to empty string to hide. Hero-forced renders never draw a title.
subtitle
string (optional)Description text below heading. Full variant only.
showViewAll
boolean (default false)Full variant only. Renders a "View all case studies" button below the carousel.
Card styling
.card-carousel-item
var(--gradient-card-surface) — a neutral 5% wash of --foreground ending on transparent — plus a --border-subtle hairline. No accent anywhere. Shared with .card-blog, .card-featured and .card-faq, so never edit the base rule..card-carousel-active
Rotating conic-gradient border animation applied only to the active (centered) card in both variants..carousel-edge-mask
Fades the track's own pixels at both clip edges (≥1280px), so cards and ghosts dissolve while the ground behind them — the paper sections' draft grid on the home page — stays visible.It replaced two painted overlays that flooded flat --background inward: identical card fade, but they erased whatever the section ground was drawing in a 128px strip down each side. Wraps only the track — the arrows are siblings outside it and stay solid.
.card-ghost
Covers every non-active card. Opaque, mixed from --foreground against --background, so it resolves per surface with no variants: soft tan on a paper ground, soft charcoal on the dark page.It replaced dimming the neighbours with opacity, which only worked while the ground was as dark as the card — fading a near-black card toward cream passes through grey. It crossfades rather than swapping, because the active index updates before the spring settles.
.surface-paper .card-carousel-item
background: var(--gradient-card-surface), var(--surface-ink-panel);The card's wash ends on transparent, so on the cream home-page ground the paper would show through and tint it. Layering the identical wash over the opaque --surface-ink-panel reproduces the dark-page composite exactly, so the card stays dark. A matching :hover rule exists at higher specificity — without it the shared hover rule ties on specificity and drops the opaque backing mid-hover.
.carousel-arrow
background: var(--background); 48px circle at left-14 / right-14Solid rather than a translucent wash: the arrows sit over the neighbour slot, not on the section ground, so a translucent fill would take on whatever is behind it — on a light section that meant an ink-tinted arrow over a dark card. A --background fill always contrasts with the --foreground icon on it, so it reads on whatever it overlays.
Shimmer primitive
Every shimmer bar in the skeleton is a local `ShimmerBar` — a rounded div with `bg-foreground/10 animate-pulse`. Because it's keyed off `--foreground` it follows whichever surface class is in scope, so the same bar works on the dark page and on a paper ground.
function ShimmerBar({ className }: { className?: string }) {
return (
<div
className={cn(
'rounded-sm bg-foreground/10 animate-pulse',
className
)}
/>
)
}Usage
import { CaseStudyCarousel } from '@/components/sections/CaseStudyCarousel'
import { CaseStudyGridSkeleton } from '@/components/sections/CaseStudyGridSkeleton'
// Full carousel on /work and /home
<CaseStudyCarousel caseStudies={caseStudies} showViewAll subtitle="…" />
// Compact hero variant — wrap in an lg:block container
<div className="hidden w-full lg:block lg:max-w-[540px]">
<CaseStudyCarousel caseStudies={caseStudies} variant="hero" />
</div>
// Loading skeleton (auto-picks variant by viewport)
<CaseStudyGridSkeleton />Import
import { CaseStudyCarousel } from '@/components/sections/CaseStudyCarousel'
import { CaseStudyGridSkeleton } from '@/components/sections/CaseStudyGridSkeleton'
import { WorkCarousel } from '@/components/sections/WorkCarousel'Related files
src/components/sections/CaseStudyCarousel.tsxsrc/components/sections/CaseStudyGridSkeleton.tsxsrc/components/sections/WorkCarousel.tsxsanity/schemas/caseStudy.tssrc/lib/queries.ts