Product Image Swap
Hover a product card and the photo gives way to a second shot, wiping in from the side while the first drifts away. Move off and it swaps back. On a phone, tap the photo to swap.
Trail Runner£89.00
How to use it
- Paste the snippet just before
</head>. - Put the two photos in a box and add
data-product-image-swapto it: the first is the main photo, the second the one hover shows. - To swap when anywhere on the card is hovered, put
data-product-image-swapon the card anddata-swap-photoson the photo box.
Options
Add any of these attributes to the same element.
data-effect | slide wipes the second photo in from the side; fade cross-fades. Default slide. |
data-duration | How long a swap takes. Default 550 ms. |
data-dots | Show two little dots saying which photo is showing. |
data-autoplay | It swaps by itself every couple of seconds while in view, until someone hovers or taps it. Handy for demos. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Product Image Swap: hover a product card and its photo gives way to a
// second shot (the back, a detail, someone wearing it). The second photo
// slides or fades in over the first with a gentle settle of the zoom, and
// swaps back when the pointer leaves. On touch screens a tap swaps them.
// Usage:
// <div data-product-image-swap>
// <img src="front.jpg" alt="Trail Runner">
// <img src="side.jpg" alt="">
// </div>
// The first child is the main photo, the second the one hover shows. Put
// data-product-image-swap on the card instead and mark the photo box with
// data-swap-photos to swap when anywhere on the card is hovered.
// Optional: data-effect ("slide", default, wipes the second photo in from
// the side; "fade" cross-fades), data-duration (ms, default 550),
// data-dots (show two little dots saying which photo is showing),
// data-autoplay (it swaps by itself every couple of seconds while in view,
// until someone hovers or taps it; handy for demos).
var DEFAULTS = { duration: 550, every: 2200 };
var css =
'.pis-box{position:relative;display:grid;overflow:hidden;isolation:isolate}' +
'.pis-box>:not(.pis-dots){grid-area:1/1;width:100%;height:100%;min-width:0;object-fit:cover}' +
'.pis-box>.pis-alt{position:relative;z-index:1}' +
'.pis-dots{position:absolute;left:50%;bottom:8px;z-index:2;display:flex;gap:5px;translate:-50% 0;pointer-events:none}' +
'.pis-dots i{width:6px;height:6px;border-radius:50%;background:rgba(0,0,0,.18);transition:background .3s,width .3s}' +
'.pis-dots i.pis-on{width:14px;border-radius:3px;background:rgba(0,0,0,.55)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var box = el.querySelector('[data-swap-photos]') || el;
var kids = Array.prototype.filter.call(box.children, function (c) { return !c.classList.contains('pis-dots'); });
if (kids.length < 2) return;
box.classList.add('pis-box');
var main = kids[0], alt = kids[1];
alt.classList.add('pis-alt');
alt.setAttribute('aria-hidden', 'true');
var s = el._pis = { box: box, main: main, alt: alt, on: false, timer: null, dots: null };
if (el.dataset.dots !== undefined && el.dataset.dots !== 'false') {
s.dots = document.createElement('span');
s.dots.className = 'pis-dots';
s.dots.setAttribute('aria-hidden', 'true');
s.dots.innerHTML = '<i class="pis-on"></i><i></i>';
box.appendChild(s.dots);
}
place(el, false);
var fine = window.matchMedia('(hover: hover) and (pointer: fine)');
el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch' && fine.matches) { stop(el); swap(el, true); } });
el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch' && fine.matches) swap(el, false); });
el.addEventListener('focusin', function () { swap(el, true); });
el.addEventListener('focusout', function () { swap(el, false); });
// No hover on touch screens: a tap on the photo swaps it instead.
box.addEventListener('pointerup', function (e) {
if (e.pointerType === 'mouse' && fine.matches) return;
stop(el);
swap(el, !s.on);
});
}
function effect(el) { return el.dataset.effect === 'fade' ? 'fade' : 'slide'; }
function place(el, on) {
var s = el._pis;
if (effect(el) === 'fade') {
anime.utils.set(s.alt, { opacity: on ? 1 : 0, scale: 1, clipPath: 'inset(0 0 0 0)' });
} else {
anime.utils.set(s.alt, { opacity: 1, clipPath: on ? 'inset(0 0% 0 0)' : 'inset(0 0% 0 100%)' });
}
}
function swap(el, on) {
var s = el._pis;
if (!s || s.on === on) return;
s.on = on;
if (s.dots) {
s.dots.children[0].classList.toggle('pis-on', !on);
s.dots.children[1].classList.toggle('pis-on', on);
}
s.main.setAttribute('aria-hidden', on);
s.alt.setAttribute('aria-hidden', !on);
if (reduced()) { place(el, on); return; }
var dur = +el.dataset.duration || DEFAULTS.duration;
anime.utils.remove([s.alt, s.main]);
if (effect(el) === 'fade') {
anime.animate(s.alt, { opacity: on ? 1 : 0, duration: dur, ease: 'out(2)' });
anime.animate(on ? s.alt : s.main, { scale: [1.08, 1], duration: dur * 1.6, ease: 'out(3)' });
} else if (on) {
// In from the right edge, with the photo itself drifting in a touch.
anime.animate(s.alt, { clipPath: ['inset(0 0% 0 100%)', 'inset(0 0% 0 0%)'], duration: dur, ease: 'inOut(3)' });
anime.animate(s.alt, { x: ['12%', '0%'], scale: [1.1, 1], duration: dur * 1.4, ease: 'out(3)' });
anime.animate(s.main, { x: ['0%', '-8%'], duration: dur, ease: 'inOut(3)' });
} else {
anime.animate(s.alt, { clipPath: ['inset(0 0% 0 0%)', 'inset(0 0% 0 100%)'], duration: dur, ease: 'inOut(3)' });
anime.animate(s.alt, { x: '12%', duration: dur, ease: 'inOut(3)' });
anime.animate(s.main, { x: ['-8%', '0%'], scale: [1.05, 1], duration: dur * 1.4, ease: 'out(3)' });
}
}
function stop(el) {
var s = el._pis;
if (s && s.timer) { clearInterval(s.timer); s.timer = null; }
if (s) s.stopped = true;
}
function autoplay(el) {
var s = el._pis;
new IntersectionObserver(function (entries) {
clearInterval(s.timer);
s.timer = null;
if (!entries[0].isIntersecting || s.stopped) return;
s.timer = setInterval(function () { swap(el, !s.on); }, DEFAULTS.every);
}, { threshold: 0.3 }).observe(el);
}
function init() {
document.querySelectorAll('[data-product-image-swap]').forEach(function (el) {
if (el._pisDone) return;
el._pisDone = true;
setup(el);
if (el._pis && el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false' && !reduced()) autoplay(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a class="product" href="/trail-runner" data-product-image-swap>
<div data-swap-photos>
<img src="trail-runner.jpg" alt="Trail Runner">
<img src="trail-runner-side.jpg" alt="">
</div>
<h3>Trail Runner</h3>
</a>