Every result card on the search page can render an in-place photo gallery. The visitor swipes or clicks prev/next arrows to step through up to five photos per listing without leaving the search results. The first photo is the featured image, the next four come from the gallery. It’s pure CSS scroll-snap plus a few lines of JS, no carousel library, no jQuery, lazy-loaded after the first frame.
What the slider does
- Renders inside the card thumbnail area (left side of the card).
- Shows the first photo on load; the rest lazy-load when their slide scrolls into view.
- Adds prev / next circular arrow buttons that appear on card hover (top: 50%, left/right: 6px).
- Adds a dot pagination strip at the bottom-centre of the gallery, with the active dot stretched into a pill.
- Mouse-wheel scroll on the gallery snaps to the next/previous photo via CSS
scroll-snap-type. - Falls back gracefully: listings with one photo show a plain image (no arrows, no dots).
Configuring photos on a listing
Two data sources are combined:
- The featured image set via the standard WordPress “Set featured image” sidebar block. This is always slide 1.
- The gallery stored in
_sdp_gallerypostmeta as an array of attachment IDs. Set this via the Custom Fields tab on the listing edit screen, or via the standard WP media uploader if the theme exposes it.
The Search::format_listing() method assembles the search response by reading the featured image URL, then walking the first four valid attachment IDs from _sdp_gallery and resolving each to a medium size URL. The combined array is returned as row.gallery in the JSON.
How the slider renders
When buildThumb() in sdp-search.js receives a row with gallery.length > 1, it builds a .sdp-d__card-slider wrapper holding a .sdp-d__card-slider-track flex container of <img> elements, then attaches prev/next arrow buttons and a dot pagination strip. The track uses overflow-x:auto; scroll-snap-type:x mandatory; scroll-behavior:smooth, and each image is scroll-snap-align:start so the snap stops at exactly one image per swipe.
The arrow buttons are position:absolute; they’re invisible by default (opacity:0) and fade in on card hover or keyboard focus. Clicking calculates the next snap target from the track’s scroll position and width, then triggers track.scrollTo({ left:nextX, behavior:'smooth' }). The dot strip listens to the track’s scroll event and re-marks the active dot when the snap settles.
Performance and lazy loading
Only slide 1 uses loading="eager"; slides 2-5 are loading="lazy". So a search page with 12 cards triggers 12 image fetches on initial paint (one per featured image), not 60. Additional images load only when the visitor scrolls a particular gallery into a non-zero position. That keeps the page weight close to what it’d be with single-photo cards while giving the experience of richer browsing.
If you’re enabling this on a large directory (hundreds of cards across pagination), use a CDN for image hosting so the lazy-loaded photos don’t cluster on your origin. Cloudflare’s free tier handles it well; for premium loads, BunnyCDN’s image-optimisation suite is worth the few dollars a month.
FAQ
Can I disable the slider on a particular page?
The slider activates whenever the listing has more than one photo. To force the single-photo render, leave _sdp_gallery empty on that listing. There’s no per-page toggle.
What if the featured image is one of the gallery photos?
The format-listing method dedupes by URL. If the featured image URL is also in the gallery, we skip it so slide 1 is the featured image and slide 2 is the next distinct gallery photo.
Does it work on the single-listing page too?
The single-listing page has its own full-screen lightbox gallery (the original implementation, predating the card slider). They’re separate components. The card slider is search-results-only; the single page gives the visitor the lightbox view.
Is the swipe gesture native or a library?
Native CSS scroll-snap. No touch event handlers, no synthetic swipe detection, no library. The browser handles the scroll inertia and the snap stop. That’s why it feels right on iOS Safari (the hardest target for this) without any tuning.