← Visual Library / Carousels

Thumbnail strip + main view

Big main image, small thumbnails below. Click a thumb to swap the main view. The pattern every real estate site uses.

Carousels Built JS-free
Use when — the user needs to compare several views of the same thing (property, product) at full size with quick navigation. Airbnb, Zillow, every real estate listing. Works great for product detail pages too.

Live demo

How it works

Hidden radio inputs hold which slide is active. The labels styled as thumbnails are the touch targets. The :has() selector on the parent watches which radio is checked and reveals the matching slide. Pure CSS — no JS, works in iOS Quick Look.

The code (HTML)

<div class="gallery">
  <input type="radio" name="g" id="g1" checked>
  <input type="radio" name="g" id="g2">
  ...
  <div class="stage">
    <div class="slide s1">Living room</div>
    <div class="slide s2">Kitchen</div>
    ...
  </div>
  <div class="thumbs">
    <label for="g1"></label>
    <label for="g2"></label>
    ...
  </div>
</div>

The code (CSS — the key bits)

.gallery .slide { position: absolute; inset: 0; opacity: 0; transition: opacity 0.4s; }
.gallery:has(#g1:checked) .s1,
.gallery:has(#g2:checked) .s2,
.gallery:has(#g3:checked) .s3 { opacity: 1; }

.thumbs label { opacity: 0.65; border: 2px solid transparent; }
.gallery:has(#g1:checked) label[for="g1"] { opacity: 1; border-color: var(--accent); }

Variants worth considering

Crossfade vs slide — swap the opacity transition for a horizontal transform: translateX if you want the images to slide instead of fade. Slide feels punchier on mobile, fade feels more premium on desktop.

Vertical thumb rail — for portrait properties, put thumbs in a left/right column instead of below. Use grid-template-columns: 80px 1fr; on the gallery container.

Zoom on hover — wrap each slide's background in an inner .slide-img div and apply transform: scale(1.05) on hover. Adds polish on desktop, harmless on mobile.

Compatibility: :has() needs Safari 15.4+, Chrome 105+, Firefox 121+. Older browsers fall back to first-slide-only — acceptable degradation. · Code playbook cross-ref: vault/Topics/web-patterns.md § Pattern 5 (Image accordion with :has()).