Gallery
Responsive image and video galleries with slideshow navigation.
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel"
const slides = [
{ src: "/images/gradients/lilac.webp", alt: "Soft blue and indigo gradient", caption: "Blue hour" },
{ src: "/images/gradients/coral.webp", alt: "Soft coral and amber gradient", caption: "First light" },
{ src: "/images/gradients/aurora.webp", alt: "Soft violet and pink gradient", caption: "Night bloom" },
]
export default function GallerySlideshow() {
return (
<section className="mx-auto max-w-5xl">
<div className="mb-6 flex items-end justify-between gap-6">
<div>
<p className="text-sm font-medium text-muted-foreground">Journal 04</p>
<h2 className="mt-1 text-2xl font-semibold tracking-tight">Scenes from the road</h2>
</div>
<p className="hidden max-w-xs text-sm text-muted-foreground md:block">
A visual diary from twelve days moving west.
</p>
</div>
<Carousel opts={{ loop: true }} className="px-10 md:px-12">
<CarouselContent>
{slides.map((slide, index) => (
<CarouselItem key={slide.src}>
<figure className="relative aspect-[16/9] overflow-hidden rounded-lg bg-muted">
<img src={slide.src} alt={slide.alt} className="size-full object-cover" />
<figcaption className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-foreground/70 to-transparent p-5 pt-16 text-sm text-background">
{String(index + 1).padStart(2, "0")} — {slide.caption}
</figcaption>
</figure>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-0" />
<CarouselNext className="right-0" />
</Carousel>
</section>
)
}
const images = [
{ src: "/images/gradients/lilac.webp", alt: "Blue and indigo gradient", className: "md:col-span-2 md:row-span-2" },
{ src: "/images/gradients/coral.webp", alt: "Coral and amber gradient", className: "" },
{ src: "/images/gradients/aurora.webp", alt: "Violet and pink gradient", className: "" },
{ src: "/images/gradients/ember.webp", alt: "Orange and red gradient", className: "md:col-span-2" },
]
export default function ImageGallery() {
return (
<section className="mx-auto max-w-6xl">
<div className="mb-8 max-w-xl">
<p className="text-sm font-medium text-muted-foreground">Selected work</p>
<h2 className="mt-2 text-3xl font-semibold tracking-tight">A study in light and motion</h2>
</div>
<div className="grid auto-rows-[14rem] grid-cols-1 gap-3 md:grid-cols-4">
{images.map((image) => (
<figure
key={image.src}
className={`group relative overflow-hidden rounded-lg bg-muted ${image.className}`}
>
<img
src={image.src}
alt={image.alt}
loading="lazy"
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<figcaption className="sr-only">{image.alt}</figcaption>
</figure>
))}
</div>
</section>
)
}
const media = [
{ src: "/images/gradients/lilac.webp", alt: "Blue hour study", type: "Image", duration: "" },
{
src: "https://videos.pexels.com/video-files/2715412/2715412-hd_1920_1080_30fps.mp4",
poster: "/images/gradients/ember.webp",
alt: "Sparks moving through smoke",
type: "Film",
duration: "01:42",
},
{ src: "/images/gradients/aurora.webp", alt: "Night bloom study", type: "Image", duration: "" },
]
export default function MixedMediaGallery() {
return (
<section className="mx-auto max-w-6xl">
<div className="mb-6 flex items-end justify-between gap-4">
<div>
<p className="text-sm text-muted-foreground">Campaign archive</p>
<h2 className="mt-1 text-2xl font-semibold tracking-tight">Still & moving</h2>
</div>
<a href="#" className="text-sm font-medium underline-offset-4 hover:underline">View all work</a>
</div>
<div className="grid gap-6 md:grid-cols-3">
{media.map((item) => (
<article key={item.src} className="group">
<div className="relative aspect-[4/5] overflow-hidden rounded-lg bg-muted">
{item.type === "Film" ? (
<video
src={item.src}
poster={item.poster}
autoPlay
muted
loop
playsInline
controls
aria-label={item.alt}
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<img
src={item.src}
alt={item.alt}
className="size-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
)}
</div>
<div className="mt-3 flex items-center justify-between gap-4 text-sm">
<p className="font-medium">{item.alt}</p>
<p className="text-muted-foreground">{item.duration || item.type}</p>
</div>
</article>
))}
</div>
</section>
)
}