Row Collapse
A removed list item slides aside and fades, then its row folds shut, so the items below glide up to close the gap instead of jumping.
PPriyaLunch on Friday?9:41
PPromo Weekly48 hours only: 30% off9:12
SSamHere are the photos8:55
BBuild botAll checks passed8:30
How to use it
- Paste the snippet just before
</head>. - Add
data-row-collapseto a list item to remove it once it’s been in view, or - call
animacious.rowCollapse(item, done)when it’s deleted, and remove it indone.
Options
Add any of these attributes to the same element.
data-to | The way it slides as it fades: right, left, or none to shrink in place. Default right. |
data-duration | How long the slide and the collapse take together. Default 700 ms. |
data-delay | How long it stays in view before it goes. Default 1200 ms. |
data-loop | Open the gap again, fade back in, hold and collapse again. |
data-hold | How long it stays between removals, when looping. Default 1400 ms. |
It animates the item’s height, padding, margins and borders down to nothing, then sets display: none. Space your rows with margins rather than a flex or grid gap, as a gap around the item can’t close.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Row Collapse: a removed list item slides aside and fades, then its row
// folds shut so the items around it glide together to close the gap.
// Usage: <li data-row-collapse>…</li>
// It plays once the item has been in view for data-delay ms. Without
// data-loop it ends hidden (display: none), so call
// animacious.rowCollapse(el, done) and remove it in done if you like.
// Optional: data-to (the way it slides as it fades: "right", default,
// "left", or "none" to shrink in place), data-duration (ms, default 700),
// data-delay (ms in view before it goes, default 1200), data-loop (open
// the gap again, fade back in, hold and collapse again), data-hold (ms to
// stay between removals when looping, default 1400).
// Script use: animacious.rowCollapse(el, done) plays it on any element
// now, using its data-* options, and calls done afterwards.
var DEFAULTS = { to: 'right', duration: 700, delay: 1200, hold: 1400 };
var BOX = ['height', 'paddingTop', 'paddingBottom', 'marginTop', 'marginBottom', 'borderTopWidth', 'borderBottomWidth'];
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Hold the row at its current size so those sizes can be animated.
function freeze(el) {
var cs = getComputedStyle(el);
var box = {};
BOX.forEach(function (k) { box[k] = cs[k]; });
el._rwcBox = box;
BOX.forEach(function (k) { el.style[k] = box[k]; });
el.style.minHeight = '0px';
el.style.overflow = 'hidden';
return box;
}
function unfreeze(el) {
BOX.concat(['minHeight', 'overflow']).forEach(function (k) { el.style[k] = ''; });
}
function leave(el, done) {
var d = el.dataset;
var t = +d.duration || DEFAULTS.duration;
var to = d.to || DEFAULTS.to;
if (reduced()) {
el.style.display = 'none';
if (done) done();
return;
}
anime.utils.set(el, { x: 0, scale: 1, opacity: 1 });
var slide = to === 'none' ? 0 : Math.min(60, el.offsetWidth * 0.15) * (to === 'left' ? -1 : 1);
freeze(el);
var shut = {};
BOX.forEach(function (k) { shut[k] = '0px'; });
shut.duration = t * 0.55;
shut.ease = 'inOut(3)';
anime.createTimeline({
onComplete: function () {
if (done) done();
}
})
// The item shrinks and slides aside…
.add(el, {
x: { to: slide, ease: 'in(2)' },
scale: { to: to === 'none' ? 0.85 : 0.94, ease: 'in(2)' },
opacity: { to: 0, ease: 'in(1.5)' },
duration: t * 0.45
})
// …then the gap it leaves closes up.
.add(el, shut);
}
function back(el, done) {
var box = el._rwcBox;
var open = {};
BOX.forEach(function (k) { open[k] = box[k]; });
open.duration = 450;
open.ease = 'inOut(3)';
anime.utils.set(el, { x: 0, scale: 0.94 });
open.onComplete = function () {
unfreeze(el);
anime.animate(el, { opacity: 1, scale: 1, duration: 400, ease: 'out(2)', onComplete: done });
};
anime.animate(el, open);
}
function cycle(el) {
var d = el.dataset;
var s = el._rwc;
leave(el, function () {
if (d.loop === undefined || d.loop === 'false') {
unfreeze(el);
el.style.display = 'none';
return;
}
// Open the gap again, then go again once it's on screen.
setTimeout(function () {
back(el, function () {
setTimeout(function () {
if (s.visible) cycle(el);
else s.waiting = true;
}, +d.hold || DEFAULTS.hold);
});
}, 700);
});
}
window.animacious = window.animacious || {};
window.animacious.rowCollapse = function (el, done) {
leave(el, function () {
unfreeze(el);
el.style.display = 'none';
if (done) done(el);
});
};
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._rwc;
s.visible = e.isIntersecting;
if (!e.isIntersecting) return;
if (!s.started) {
s.started = true;
var wait = e.target.dataset.delay;
setTimeout(function () { cycle(e.target); }, wait === undefined ? DEFAULTS.delay : +wait || 0);
} else if (s.waiting) {
s.waiting = false;
cycle(e.target);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-row-collapse]').forEach(function (el) {
if (el._rwc) return;
el._rwc = {};
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><ul class="inbox">
<li>Lunch on Friday?</li>
<li data-row-collapse>48 hours only: 30% off</li>
<li>Here are the photos</li>
</ul>
<!-- or on demand, when a delete button is clicked: -->
<script>
button.addEventListener('click', function () {
animacious.rowCollapse(item, function () { item.remove(); });
});
</script>