HTML
Copy
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-gal-muted uppercase">Main photo with a thumbnail strip</span>
<section role="region" aria-roledescription="carousel" aria-label="Alder House photos" data-carousel data-loop>
<div class="mb-[18px] flex flex-wrap items-end justify-between gap-x-6 gap-y-2"><div><p class="text-xs font-bold tracking-[.12em] text-gal-accent uppercase">For sale · Hampstead</p><h2 class="mt-1 text-[1.45rem] font-bold tracking-[-.02em] sm:text-[1.7rem]">Alder House</h2></div><p class="text-[1.35rem] font-bold sm:text-right">£1,850,000 <span class="mt-0.5 block text-[.88rem] font-medium text-gal-muted">4 bed · 3 bath · 212 m²</span></p></div>
<div class="relative overflow-hidden rounded-[18px] bg-[#dfe5e2]">
<div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-6 focus-visible:outline-white" data-track tabindex="0">
<div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="1 of 5">
<img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" alt="Two-storey timber and glass house lit from inside at dusk, with a lawn and a large tree">
<span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">1 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Garden elevation at dusk</span>
</div>
<div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="2 of 5">
<img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="House front with vertical timber cladding, black metal panels and a gravel drive">
<span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">2 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Street front</span>
</div>
<div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="3 of 5">
<img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="Open living room with a white sofa, a walnut feature wall and a glass wall to the garden">
<span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">3 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Living room</span>
</div>
<div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="4 of 5">
<img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="Bedroom with an upholstered bed, dark walls and a brass ceiling light">
<span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">4 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Main bedroom</span>
</div>
<div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="5 of 5">
<img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="White bathroom with a walk-in glass shower and a long vanity">
<span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">5 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Family bathroom</span>
</div>
</div>
<button class="absolute top-1/2 z-[1] -mt-[19px] grid size-[38px] cursor-pointer place-items-center rounded-full bg-white/94 text-gal-ink shadow-[0_6px_18px_rgba(0,0,0,.18)] transition-transform duration-200 hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] sm:-mt-[22px] sm:size-11 motion-reduce:transition-none motion-reduce:hover:scale-100 left-3.5" type="button" data-prev data-controls hidden aria-label="Previous photo"><svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
<button class="absolute top-1/2 z-[1] -mt-[19px] grid size-[38px] cursor-pointer place-items-center rounded-full bg-white/94 text-gal-ink shadow-[0_6px_18px_rgba(0,0,0,.18)] transition-transform duration-200 hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] sm:-mt-[22px] sm:size-11 motion-reduce:transition-none motion-reduce:hover:scale-100 right-3.5" type="button" data-next data-controls hidden aria-label="Next photo"><svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
</div>
<div class="mt-2.5 grid grid-cols-5 gap-1.5 sm:gap-2.5" data-controls hidden>
<button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 1: Garden elevation at dusk"><img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
<button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 2: Street front"><img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
<button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 3: Living room"><img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
<button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 4: Main bedroom"><img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
<button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 5: Family bathroom"><img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
</div>
</section></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-gal-muted uppercase">Side thumbnails</span>
<section class="grid gap-3 sm:grid-cols-[96px_1fr]" role="region" aria-roledescription="carousel" aria-label="Garden suite photos" data-carousel>
<div class="min-w-0 overflow-hidden rounded-[18px] bg-[#e7dfd8] sm:col-start-2 sm:row-start-1">
<div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-6 focus-visible:outline-white" data-track tabindex="0">
<div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="1 of 4">
<img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" alt="Infinity pool with cabanas and palm trees at dusk">
</div>
<div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="2 of 4">
<img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" loading="lazy" alt="Hotel bedroom with a low wooden bed, a carved headboard and garden windows">
</div>
<div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="3 of 4">
<img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" loading="lazy" alt="Bedroom with a wooden bench and tall doors open onto a terrace">
</div>
<div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="4 of 4">
<img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" loading="lazy" alt="Hotel room with a wide bed, warm bedside lamps and a large headboard">
</div>
</div>
</div>
<div class="flex gap-3 sm:col-start-1 sm:row-start-1 sm:flex-col" data-controls hidden>
<button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 1: Infinity pool"><img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
<button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 2: Garden suite"><img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
<button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 3: Terrace doors"><img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
<button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 4: Twin room"><img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
</div>
</section></div>
JavaScript
Copy
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
const track = root.querySelector('[data-track]');
const slides = [...track.children];
const dots = [...root.querySelectorAll('[data-dot]')];
const prev = root.querySelector('[data-prev]');
const next = root.querySelector('[data-next]');
const loop = root.hasAttribute('data-loop');
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
const current = () => Math.round(track.scrollLeft / step());
const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
const move = (dir) => {
if (loop && dir > 0 && atEnd()) return go(0);
if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
go(current() + dir * perView);
};
const sync = () => {
const i = current();
dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
if (!loop && next) next.setAttribute('aria-disabled', atEnd());
};
prev?.addEventListener('click', () => move(-1));
next?.addEventListener('click', () => move(1));
dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
root.classList.add('is-ready');
sync();
});