How to Add a Swipeable Mobile Gallery Without Touching the Theme
Add a mobile image slider to WordPress listing pages with a scoped JavaScript snippet that collects existing images and handles lazy loading.
How to add a swipeable mobile gallery without touching the theme
Goal
Prerequisites
- [ ] A page where all the images already exist in the DOM (as
<img>tags and/or CSSbackground-image), even if hidden on mobile. - [ ] A way to inject a footer script that loads only on the target pages (for example a code-snippets plugin scoped to the single-listing template).
- [ ] Awareness of any performance plugin that removes "unused" CSS for anonymous visitors — it will affect your approach (see Configuration).
- [ ] A private/incognito window for testing a true first, uncached load.
Steps
-
1
Confirm hidden images
Confirm the images are on the page but hidden. Inspect the mobile view. If the thumbnails exist in the DOM but carry a "hidden on mobile" class, you don't need new data — you only need to re-present what's already there. That's what makes a snippet-only fix possible.
-
2
Collect all images
Collect every image on the page, from both sources. Gather
<img>srcvalues and URLs parsed out of inlinebackground-imagestyles. Real pages hide the gallery in both places. -
3
Filter by filename
Filter out the noise by base filename. Dedupe by the file's base name so the same photo served at different sizes counts once, and skip avatars (for example
150x150thumbnails), similar-listing loops, and header/footer chrome. You want the property's own images, nothing else. -
4
Build slider
Build a horizontal scroll-snap slider and insert it at the hero position. Create a scroll container of the collected images and place it where the single mobile image sits, then hide that single image on mobile only:
-
5
Inject CSS via JS
Inject the CSS through JavaScript, not a stylesheet. If a performance plugin strips unused CSS for anonymous visitors, it will delete styles it can't see referenced — including styles for markup you add at runtime. Ship the CSS the same way you ship the markup: create a
<style>element in the script so the optimiser can't strip it. -
6
Use generic selectors
Target no hardcoded IDs so one snippet covers every template. Select by structure and generic classes, not by a specific listing's element IDs. A generic snippet automatically covers the buy, rent, and sell templates with no per-template copies.
-
7
Handle lazy loading
Handle lazy-loaded images with run-now-then-retry. The footer script runs after
DOMContentLoaded, but lazy-loaded images may not be present yet. Run the collector immediately, retry on a short interval until images appear, and run once more onwindow.load:
const urls = new Set();
document.querySelectorAll('img').forEach(el => el.src && urls.add(el.src));
document.querySelectorAll('[style*="background-image"]').forEach(el => {
const m = el.style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
if (m) urls.add(m[1]);
});
@media (max-width:767px){
.myslider{display:flex;overflow-x:auto;scroll-snap-type:x mandatory}
.myslider img{flex:0 0 100%;scroll-snap-align:start}
.original-hero{display:none}
}
function build(){ /* steps 2–4 */ }
build();
let tries = 0;
const t = setInterval(() => { if (build() || ++tries > 20) clearInterval(t); }, 250);
addEventListener('load', build);
Configuration
| Setting | Recommended | Tradeoff |
|---|---|---|
| Scope of the snippet | Load on the single-listing template only | Avoids running gallery code on unrelated pages |
| CSS delivery | Injected via JS | Survives "remove unused CSS"; slightly later paint than a static sheet |
| Image dedupe key | Base filename | Collapses resized duplicates; assumes filenames are meaningful |
| Skip list | Avatars, loop items, chrome | One place to tune false positives |
| Mobile breakpoint | max-width:767px |
Match your theme's mobile breakpoint |
Verification
- On a mobile width, in a private window on first load, the single hero image is replaced by a slider and swiping moves between all the property's images.
- Desktop layout is unchanged.
- No header overlap and no empty gap below the slider.
- The gallery still renders for a logged-out visitor (the case where unused-CSS removal is most aggressive).
- The same snippet works on a buy, a rent, and a sell listing with no edits.
Common problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Slider styled correctly for you, unstyled for logged-out visitors | Performance plugin stripped the CSS | Inject the CSS via JS instead of a stylesheet |
| Slider shows only some images | Ran once before lazy-loaded images arrived | Add the retry loop and the load re-run |
| Same photo appears several times | Resized variants counted separately | Dedupe by base filename |
| Avatars or unrelated thumbnails in the gallery | Collector too broad | Extend the skip list (sizes, loop items, chrome) |
| Works on one listing type, not another | Selector tied to specific IDs | Select by structure/classes, no hardcoded IDs |
Production checklist
- [ ] Snippet scoped to only the pages that need it.
- [ ] CSS injected via JS and verified for anonymous visitors.
- [ ] Retry-until-present logic covers lazy-loaded images.
- [ ] Dedupe and skip lists tuned against real listings.
- [ ] Verified across every listing template and on a real first, uncached mobile load.
- [ ] Desktop layout confirmed untouched.
Related / Next steps
- If first paint on mobile still needs a nudge, a single layout reflow after insertion usually settles it; diagnose on a real device, not just an emulator.
- This pattern generalises: any time the data you need is already on the page but hidden, a scoped footer snippet re-presents it without a theme change or a redeploy.
Ready to Implement This Guide?
Our team can implement these strategies for you, tailored to your specific business needs.
Schedule Consultation