← Buttons

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

  1. Paste the snippet just before </head>.
  2. Add data-trash-delete-button to a button. The script adds the bin icon in front of your label.
  3. Delete the item when it fires its delete event.

Options

Add any of these attributes to the same element.

data-doneLabel once deleted. Default “Deleted”.
data-holdHow long the deleted label stays. Default 1500 ms.
data-autoplayIt 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.

1 · Snippet
<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>
2 · Your HTML
<button id="del" data-trash-delete-button>Delete</button>
<script>
  document.getElementById("del").addEventListener("delete", () => {
    fetch("/files/42", { method: "DELETE" });
  });
</script>