A product page with scroll-to-zoom and a frosted backdrop. Closing lands on whichever photo you were viewing.
Photos by Mojtaba Fahiminia on Unsplash.
Field Sneaker
Suede and leather · Sand / Tangerine
$128
Size (EU)
import { cn } from 'cn'
import * as React from 'react'
import { Chrome, Lightbox, Slides } from '../lightbox'
// A product series by Mojtaba Fahiminia on Unsplash, 3200px on the long side so a 4× zoom stays sharp.
const shot = (name: string, width: number, height: number, alt: string) => ({
src: `/photos/${name}.webp`,
thumb: `/photos/${name}-thumb.webp`,
width,
height,
alt,
})
const photos = [
shot('sneaker-balance', 2286, 3200, 'The sneaker balanced on two stones'),
shot('sneaker-pair', 2286, 3200, 'The pair, one standing on its heel'),
shot('sneaker-side', 3200, 2286, 'Side view: suede toe, leather upper'),
shot('sneaker-stones', 2286, 3200, 'The pair on stones with a dry branch'),
]
const sizes = [39, 40, 41, 42, 43, 44, 45]
export default function Product() {
const [selected, setSelected] = React.useState(0)
const [size, setSize] = React.useState(42)
return (
// Following the viewer's index means closing flies the photo back into the main image, whichever one you're on.
<Lightbox.Root items={photos} onIndexChange={setSelected} wheelToZoom>
<div className="grid gap-5 sm:grid-cols-[1fr_11rem]">
<div className="flex flex-col-reverse gap-2 sm:flex-row">
<div className="flex gap-2 sm:flex-col">
{photos.map((photo, i) => (
<button
key={photo.src}
type="button"
onClick={() => setSelected(i)}
aria-label={`Show photo ${i + 1}`}
aria-current={i === selected}
className={cn(
'size-12 shrink-0 overflow-hidden rounded-lg outline-offset-2 transition',
i === selected ? 'outline-2 outline-neutral-900 dark:outline-white' : 'opacity-60 hover:opacity-100',
)}
>
<img src={photo.thumb} alt="" className="size-full object-cover" />
</button>
))}
</div>
<Lightbox.Trigger
index={selected}
className="group relative flex-1 cursor-zoom-in overflow-hidden rounded-xl bg-neutral-100 outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 dark:bg-neutral-900"
>
<img src={photos[selected]!.thumb} alt={photos[selected]!.alt} className="aspect-4/5 w-full object-cover" />
</Lightbox.Trigger>
</div>
<div className="flex flex-col">
<h3 className="text-base font-medium">Field Sneaker</h3>
<p className="text-neutral-500">Suede and leather · Sand / Tangerine</p>
<p className="mt-2 font-medium tabular-nums">$128</p>
<p className="mt-5 mb-2 text-xs text-neutral-500">Size (EU)</p>
<div className="grid grid-cols-4 gap-1.5">
{sizes.map((s) => (
<button
key={s}
type="button"
onClick={() => setSize(s)}
aria-pressed={s === size}
className={cn(
'rounded-md border py-1.5 tabular-nums transition',
s === size
? 'border-neutral-900 bg-neutral-900 text-white dark:border-white dark:bg-white dark:text-neutral-900'
: 'border-neutral-200 hover:border-neutral-400 dark:border-neutral-800 dark:hover:border-neutral-600',
)}
>
{s}
</button>
))}
</div>
<button
type="button"
className="mt-4 rounded-full bg-neutral-900 py-2.5 font-medium text-white dark:bg-white dark:text-neutral-900"
>
Add to bag
</button>
</div>
</div>
<Lightbox.Portal>
<Lightbox.Backdrop className="z-50 bg-white/10 backdrop-blur-sm dark:bg-neutral-950/70" />
<Lightbox.Popup className="group z-50 text-neutral-900 outline-none dark:text-white">
<Slides className="sm:pb-32" />
<Chrome>
<Lightbox.Close
aria-label="Close"
className="pointer-events-auto absolute top-3 right-3 grid size-10 place-items-center rounded-full bg-neutral-900/5 outline-none hover:bg-neutral-900/10 focus-visible:ring-2 focus-visible:ring-neutral-400 dark:bg-white/10 dark:hover:bg-white/20"
>
<svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth={2} aria-hidden>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</Lightbox.Close>
<p className="absolute inset-x-0 bottom-22 text-center text-xs transition-opacity group-data-zoomed:opacity-0">
<span className="pointer-coarse:hidden">Scroll or double-click to zoom</span>
<span className="hidden pointer-coarse:inline">Pinch or double-tap to zoom</span>
</p>
<Lightbox.Thumbnails className="pointer-events-auto absolute inset-x-0 bottom-0 flex gap-2 px-[50%] pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
{photos.map((photo, i) => (
<Lightbox.Thumbnail
key={photo.src}
index={i}
className="size-12 shrink-0 overflow-hidden rounded-lg opacity-50 transition-opacity outline-none hover:opacity-80 data-active:opacity-100"
>
<img src={photo.thumb} alt="" className="size-full object-cover" draggable={false} />
</Lightbox.Thumbnail>
))}
<Lightbox.ThumbnailIndicator className="rounded-lg outline-2 outline-offset-2 outline-neutral-900 dark:outline-white" />
</Lightbox.Thumbnails>
</Chrome>
</Lightbox.Popup>
</Lightbox.Portal>
</Lightbox.Root>
)
}