Trash Delete Button
Press delete and the lid of the little bin flips open, a sheet of paper tumbles in, and the lid slams shut with a bump that jolts the button. The label changes to “Deleted”, then quietly changes back.
How to use it
- Paste the snippet just before
</head>. - Add
data-trash-delete-buttonto a button. The script adds the bin icon in front of your label. - Delete the item when it fires its
deleteevent.
Options
Add any of these attributes to the same element.
data-done | Label once deleted. Default “Deleted”. |
data-hold | How long the deleted label stays. Default 1500 ms. |
data-autoplay | It deletes by itself every few seconds while in view, without firing the event. Handy for demos. |
The icon uses the text colour. Leave a little room above the button for the paper to fall from. With reduced motion it just swaps the label.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Trash Delete Button: press it and the lid of the bin icon flips open, a
// little sheet of paper drops in, and the lid shuts with a bump while the
// label changes to "Deleted". Then it quietly turns back.
// Usage: <button data-trash-delete-button>Delete</button>
// Optional: data-done (label once deleted, default "Deleted"), data-hold
// (ms the deleted state stays, default 1500), data-autoplay (it deletes
// by itself every few seconds while in view, without firing the event;
// handy for demos).
// Events: fires "delete" on the button when pressed.
// With reduced motion it just swaps the label.
var DEFAULTS = { done: 'Deleted', hold: 1500, every: 4200 };
var count = 0;
var css =
'.tdb{position:relative}' +
'.tdb-icon{width:1.3em;height:1.3em;flex:none;overflow:visible;margin-right:.5em}' +
'.tdb-lid,.tdb-body{transform-box:fill-box}' +
'.tdb-lid{transform-origin:8% 100%}' +
'.tdb-body{transform-origin:50% 100%}' +
'.tdb-paper{opacity:0}' +
'.tdb-labels{display:inline-grid}' +
'.tdb-labels>span{grid-area:1/1;justify-self:center;white-space:nowrap}' +
'.tdb-done{opacity:0}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function icon(id) {
// Paper falls through a clip that ends at the bin's rim, so it vanishes
// inside the bin.
return '<svg class="tdb-icon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<defs><clipPath id="' + id + '"><rect x="-20" y="-40" width="64" height="49.5"/></clipPath></defs>' +
'<g clip-path="url(#' + id + ')"><g class="tdb-paper"><path d="M9.2 1h4.3l2.3 2.3V9.5H9.2z" fill="currentColor" fill-opacity=".25"/>' +
'<path d="M10.8 4.5h3.2M10.8 6.8h3.2" stroke-width="1.3"/></g></g>' +
'<g class="tdb-body"><path d="M5.5 9.5h13l-1.1 11a1.6 1.6 0 0 1-1.6 1.4H8.2a1.6 1.6 0 0 1-1.6-1.4z"/>' +
'<path d="M10 13v5.5M14 13v5.5"/></g>' +
'<g class="tdb-lid"><path d="M3.5 7.5h17"/><path d="M9.5 7.5V5.6c0-.6.5-1.1 1.1-1.1h2.8c.6 0 1.1.5 1.1 1.1v1.9"/></g></svg>';
}
function setup(el) {
var d = el.dataset;
var hold = +d.hold || DEFAULTS.hold;
el.classList.add('tdb');
var labels = document.createElement('span');
labels.className = 'tdb-labels';
var label = document.createElement('span');
label.className = 'tdb-label';
while (el.firstChild) label.appendChild(el.firstChild);
var done = document.createElement('span');
done.className = 'tdb-done';
done.setAttribute('aria-hidden', 'true');
done.textContent = d.done || DEFAULTS.done;
labels.appendChild(label);
labels.appendChild(done);
el.insertAdjacentHTML('afterbegin', icon('tdb-clip-' + (++count)));
el.appendChild(labels);
var lid = el.querySelector('.tdb-lid');
var body = el.querySelector('.tdb-body');
var paper = el.querySelector('.tdb-paper');
var busy = false;
function swap(toDone, t) {
var show = toDone ? done : label, hide = toDone ? label : done;
hide.setAttribute('aria-hidden', 'true');
show.removeAttribute('aria-hidden');
anime.animate(hide, { opacity: 0, y: toDone ? -8 : 8, duration: 200 * t, ease: 'in(2)' });
anime.animate(show, { opacity: [0, 1], y: [toDone ? 8 : -8, 0], duration: 300 * t, delay: 120 * t, ease: 'out(3)' });
}
function run() {
if (busy) return;
busy = true;
if (reduced()) {
swap(true, 0);
setTimeout(function () { swap(false, 0); busy = false; }, hold);
return;
}
var tl = anime.createTimeline({
onComplete: function () {
setTimeout(function () {
swap(false, 1);
setTimeout(function () { busy = false; }, 420);
}, hold);
}
});
// Lid flips open, paper drops in, lid shuts with a bump.
tl.add(lid, { rotate: -38, y: -2.5, x: -1, duration: 260, ease: 'out(3)' })
.add(paper, { opacity: [0, 1], y: [-12, -6], rotate: [-12, -4], duration: 200, ease: 'out(2)' }, 120)
.add(paper, { y: 13, rotate: 6, duration: 340, ease: 'in(2.4)' }, 330)
.add(lid, { rotate: 0, y: 0, x: 0, duration: 420, ease: 'outBack(2.4)' }, 600)
.add(body, { scaleY: [1, 0.86, 1.04, 1], scaleX: [1, 1.08, 0.98, 1], duration: 420, ease: 'out(2)' }, 620)
.add(el, { rotate: [0, -3, 2.5, -1.5, 0], duration: 420, ease: 'out(2)' }, 640)
.call(function () { anime.utils.set(paper, { opacity: 0, y: 0, rotate: 0 }); swap(true, 1); }, 680);
}
el.addEventListener('click', function () {
if (busy) return;
el.dispatchEvent(new CustomEvent('delete', { bubbles: true }));
run();
});
if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null;
var loop = function () {
if (!el.matches(':hover')) run();
timer = setTimeout(loop, DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(loop, 700);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-trash-delete-button]').forEach(function (el) {
if (el._tdb) return;
el._tdb = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button id="del" data-trash-delete-button>Delete</button>
<script>
document.getElementById("del").addEventListener("delete", () => {
fetch("/files/42", { method: "DELETE" });
});
</script>