Sliced Image Hover
On hover, the picture splits into strips that slide apart, every other one going the opposite way, with thin gaps opening between them. The strips nearest the pointer move furthest, and they close back into one picture when it leaves.
data-axis="y"How to use it
- Paste the snippet just before
</head>. - Wrap the image in an element with
data-sliced-image-hover. Any single image, inline SVG or element works as the picture.
Options
Add any of these attributes to the same element.
data-slices | How many strips. Default 6. |
data-axis | x for upright strips that slide up and down, y for flat strips that slide sideways. Default x. |
data-distance | How far the strips slide, in px. Default 18. |
data-gap | Gap between the open strips, in px. Default 4. |
data-stagger | Delay between strips, rippling out from the pointer. Default 35 ms. |
data-autoplay | It opens and closes on its own while nobody’s hovering. Handy for demos. |
The strips are copies of the picture, each clipped to its own band, so give the wrapper a size (or let the image set it). The gaps show whatever is behind the wrapper. On touch screens, a tap opens or closes it, and it opens when it gets keyboard focus. With reduced motion turned on, the strips step apart without sliding.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Sliced Image Hover: on hover, an image splits into strips that slide
// apart, every other strip going the opposite way, with thin gaps opening
// between them. The strips nearest the pointer move furthest, and they
// close back into one picture when the pointer leaves.
// Usage: <div data-sliced-image-hover><img src="photo.jpg" alt="…"></div>
// (any single image, inline <svg> or element works as the picture).
// Optional: data-slices (how many strips, default 6), data-axis ("x" for
// upright strips that slide up and down, "y" for flat strips that slide
// sideways; default x), data-distance (how far strips slide, px, default
// 18), data-gap (gap between open strips, px, default 4), data-stagger
// (delay between strips, ms, default 35), data-autoplay (it opens and
// closes on its own while nobody is hovering; handy for demos).
// On touch screens, a tap opens or closes it.
var DEFAULTS = { slices: 6, axis: 'x', distance: 18, gap: 4, stagger: 35 };
var css =
'.sih{position:relative;overflow:hidden;isolation:isolate}' +
'.sih-src{visibility:hidden}' +
'.sih-slice{position:absolute;inset:0;pointer-events:none;will-change:transform,clip-path}' +
'.sih-slice>*{display:block;width:100%;height:100%;object-fit:cover}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function num(v, def) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : def; }
function setup(el) {
var d = el.dataset;
var n = Math.max(2, Math.min(24, Math.round(num(d.slices, DEFAULTS.slices))));
var vertical = (d.axis || DEFAULTS.axis) !== 'y'; // upright strips
var dist = num(d.distance, DEFAULTS.distance);
var gap = num(d.gap, DEFAULTS.gap);
var stagger = num(d.stagger, DEFAULTS.stagger);
var auto = d.autoplay !== undefined && d.autoplay !== 'false';
var still = reduced();
var src = el.firstElementChild;
if (!src) return;
el.classList.add('sih');
src.classList.add('sih-src');
// One copy of the picture per strip, each clipped to its own band.
var slices = [];
for (var i = 0; i < n; i++) {
var s = document.createElement('div');
s.className = 'sih-slice';
s.setAttribute('aria-hidden', 'true');
var copy = src.cloneNode(true);
copy.classList.remove('sih-src');
copy.removeAttribute('id');
s.appendChild(copy);
el.appendChild(s);
slices.push({ el: s, v: 0, cur: 0, at: 0, on: false, was: false });
}
var open = false, near = -1, raf = 0, visible = false, hover = false, touched = 0;
// Point every strip at its resting spot, staggered out from `from`.
function set(o, from) {
if (o === open && from === undefined) return;
open = o;
var now = performance.now();
var start = from === undefined ? (o ? 0 : n - 1) : from;
for (var i = 0; i < n; i++) {
var s = slices[i];
s.was = now >= s.at ? s.on : s.was;
s.on = o;
s.at = now + Math.abs(i - start) * stagger;
}
wake();
}
function target(i) {
var s = slices[i];
if (!(performance.now() >= s.at ? s.on : s.was)) return 0;
var dir = i % 2 ? 1 : -1;
// Strips near the pointer slide a little further.
var lift = near < 0 ? 1 : 0.6 + 0.4 * Math.max(0, 1 - Math.abs(i + 0.5 - near * n) / (n * 0.5));
return dir * dist * lift;
}
function draw() {
for (var i = 0; i < n; i++) {
var s = slices[i];
var p = Math.min(1, Math.abs(s.cur) / (dist || 1));
var g = gap / 2 * p;
var a = (i / n * 100).toFixed(3) + '%', b = ((n - 1 - i) / n * 100).toFixed(3) + '%';
var ga = i ? ' + ' + g.toFixed(2) + 'px' : '', gb = i < n - 1 ? ' + ' + g.toFixed(2) + 'px' : '';
var lo = 'calc(' + a + ga + ')', hi = 'calc(' + b + gb + ')';
s.el.style.clipPath = vertical ? 'inset(0 ' + hi + ' 0 ' + lo + ')' : 'inset(' + lo + ' 0 ' + hi + ' 0)';
s.el.style.transform = vertical ? 'translateY(' + s.cur.toFixed(2) + 'px)' : 'translateX(' + s.cur.toFixed(2) + 'px)';
}
}
function frame() {
raf = 0;
var busy = false, now = performance.now();
for (var i = 0; i < n; i++) {
var s = slices[i], t = target(i);
if (still) { s.cur = t; s.v = 0; }
else {
// A soft spring with a little overshoot.
s.v = (s.v + (t - s.cur) * 0.12) * 0.74;
s.cur += s.v;
}
if (Math.abs(t - s.cur) > 0.05 || Math.abs(s.v) > 0.05 || now < s.at) busy = true;
}
draw();
if (busy) raf = requestAnimationFrame(frame);
}
function wake() { if (!raf) raf = requestAnimationFrame(frame); }
function along(e) {
var b = el.getBoundingClientRect();
return vertical ? (e.clientX - b.left) / b.width : (e.clientY - b.top) / b.height;
}
el.addEventListener('pointerenter', function (e) {
if (e.pointerType === 'touch') return;
hover = true; near = along(e);
set(true, Math.floor(near * n));
});
el.addEventListener('pointermove', function (e) {
if (e.pointerType === 'touch' || !hover) return;
near = Math.max(0, Math.min(1, along(e)));
wake();
});
el.addEventListener('pointerleave', function (e) {
if (e.pointerType === 'touch') return;
hover = false; near = -1;
set(false);
});
el.addEventListener('pointerdown', function (e) {
if (e.pointerType !== 'touch') return;
touched = Date.now();
near = along(e);
set(!open, Math.floor(near * n));
});
el.addEventListener('focusin', function () { near = -1; set(true); });
el.addEventListener('focusout', function () { if (!hover) set(false); });
draw();
if (auto && !still) {
var tick = function () {
if (visible && !hover && Date.now() - touched > 4000) {
near = -1;
set(!open, open ? n - 1 : 0);
}
setTimeout(tick, open ? 1500 : 1300);
};
setTimeout(tick, 600);
}
new IntersectionObserver(function (entries) { visible = entries[0].isIntersecting; }).observe(el);
}
function init() {
document.querySelectorAll('[data-sliced-image-hover]').forEach(function (el) {
if (el._sih) return;
el._sih = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="photo" data-sliced-image-hover>
<img src="photo.jpg" alt="A mountain lake">
</div>
<!-- five flat strips that slide sideways: -->
<div data-sliced-image-hover data-axis="y" data-slices="5">…</div>