30 CSS Shake Animations21 / 30

CSS + JSMIT licensed

CSS Trash Icon Shake on Drag Over

The hungriest trash can on the web: drag a file chip over the drop zone and the lid swings open while the can rattles in anticipation; drop, and the chip is swallowed with a gulp squash. Plus a list variant where hovering a row's delete button flaps the lid — a one-hover preview of what the click will do.

Published

Live Demo
Try it

The code

<div class="shk-21-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-21a" aria-label="Drag files onto a trash can that opens and rattles">
  <div class="shk-21a__desk">
    <div class="shk-21a__files" id="shk-21a-files">
      <div class="shk-21a__chip" draggable="true" tabindex="0">draft-v1.fig</div>
      <div class="shk-21a__chip" draggable="true" tabindex="0">notes-old.md</div>
      <div class="shk-21a__chip" draggable="true" tabindex="0">unused.png</div>
    </div>
    <div class="shk-21a__zone" id="shk-21a-zone" aria-label="Delete drop zone">
      <svg class="shk-21a__can" viewBox="0 0 48 48" aria-hidden="true">
        <g class="shk-21a__lid"><rect x="10" y="10" width="28" height="5" rx="2.5" fill="currentColor"></rect><rect x="19" y="5.5" width="10" height="5" rx="2" fill="currentColor"></rect></g>
        <g class="shk-21a__body"><path d="M13 18h22l-1.8 21a3 3 0 0 1-3 2.8H17.8a3 3 0 0 1-3-2.8z" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linejoin="round"></path><line x1="20" y1="24" x2="20.6" y2="35" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></line><line x1="28" y1="24" x2="27.4" y2="35" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></line></g>
      </svg>
      <p class="shk-21a__label" id="shk-21a-label">Drag here to delete</p>
    </div>
  </div>
  <p class="shk-21a__hint" aria-hidden="true">lid transitions open (state) · can rattles (anticipation) · drop gulps (payoff)</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-21b" aria-label="List rows whose trash button flaps its lid on hover">
  <div class="shk-21b__panel">
    <p class="shk-21b__head">Uploads</p>
    <ul class="shk-21b__list" id="shk-21b-list">
      <li class="shk-21b__row"><span class="shk-21b__dot" style="--c:oklch(0.75 0.14 250)" aria-hidden="true"></span><span class="shk-21b__name">hero-final-FINAL.webp</span><span class="shk-21b__size">1.2 MB</span><button class="shk-21b__del" type="button" aria-label="Delete hero-final-FINAL.webp"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><g class="shk-21b__lid"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path></g><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></li>
      <li class="shk-21b__row"><span class="shk-21b__dot" style="--c:oklch(0.78 0.14 85)" aria-hidden="true"></span><span class="shk-21b__name">brand-tokens.json</span><span class="shk-21b__size">18 KB</span><button class="shk-21b__del" type="button" aria-label="Delete brand-tokens.json"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><g class="shk-21b__lid"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path></g><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></li>
      <li class="shk-21b__row"><span class="shk-21b__dot" style="--c:oklch(0.72 0.16 340)" aria-hidden="true"></span><span class="shk-21b__name">interview-notes.pdf</span><span class="shk-21b__size">640 KB</span><button class="shk-21b__del" type="button" aria-label="Delete interview-notes.pdf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><g class="shk-21b__lid"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path></g><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></li>
    </ul>
    <p class="shk-21b__hint" aria-hidden="true">hover a trash button — the lid flaps a preview; click removes the row</p>
  </div>
</section>
</div>
.shk-21-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.shk-21-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.shk-21a,
.shk-21a *,
.shk-21a *::before,
.shk-21a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-21a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-content: center;
  gap: 22px;
  padding: 48px 24px;
  background: linear-gradient(180deg,#f5f4f1,#e9e7e1);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.shk-21a__desk {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 56px;
}

.shk-21a__files {
  display: grid;
  gap: 12px;
}

.shk-21a__chip {
  padding: 13px 20px;
  background: #fff;
  border: 1.5px solid rgba(32,30,26,.16);
  border-radius: 12px;
  font-size: 14px;
  font-weight: 600;
  color: #201e1a;
  cursor: grab;
  box-shadow: 0 8px 18px -10px rgba(32,30,26,.35);
  transition: opacity .3s,scale .3s,box-shadow .2s;
  user-select: none;
}

.shk-21a__chip:hover {
  box-shadow: 0 12px 22px -10px rgba(32,30,26,.45);
}

.shk-21a__chip:active {
  cursor: grabbing;
}

.shk-21a__chip.is-gone {
  opacity: 0;
  scale: .6;
  pointer-events: none;
}

.shk-21a__zone {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 34px 42px;
  border: 2.5px dashed rgba(32,30,26,.3);
  border-radius: 22px;
  color: #201e1a;
  transition: border-color .25s,background .25s,color .25s;
}

.shk-21a__zone.is-hungry {
  border-color: oklch(0.6 0.21 25);
  background: oklch(0.6 0.21 25 / .07);
  color: oklch(0.52 0.2 25);
}

.shk-21a__zone * {
  pointer-events: none;
}

.shk-21a__can {
  width: 60px;
  height: 60px;
  overflow: visible;
}

.shk-21a__lid {
  transform-origin: 10px 12px;
  transform-box: view-box;
  transition: rotate .28s cubic-bezier(.34,1.4,.64,1),translate .28s cubic-bezier(.34,1.4,.64,1);
}

.shk-21a__zone.is-hungry .shk-21a__lid {
  rotate: -28deg;
  translate: -3px -7px;
}

.shk-21a__zone.is-hungry .shk-21a__body {
  animation: shk-21a-rattle .4s ease-in-out infinite;
}

.shk-21a__zone.is-gulping .shk-21a__can {
  animation: shk-21a-gulp .5s cubic-bezier(.34,1.56,.64,1);
}

.shk-21a__label {
  font-size: 13px;
  font-weight: 700;
}

.shk-21a__hint {
  text-align: center;
  font-size: 12px;
  color: rgba(32,30,26,.5);
}

@keyframes shk-21a-rattle {
  0%,
    100% {
    translate: 0 0;
    rotate: 0deg;
  }

  25% {
    translate: -1.5px 0;
    rotate: -2.5deg;
  }

  75% {
    translate: 1.5px 0;
    rotate: 2.5deg;
  }
}

@keyframes shk-21a-gulp {
  0% {
    scale: 1 1;
  }

  30% {
    scale: 1.15 .82;
  }

  60% {
    scale: .94 1.08;
  }

  100% {
    scale: 1 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-21a__zone.is-hungry .shk-21a__body,
    .shk-21a__zone.is-gulping .shk-21a__can {
    animation: none;
  }
}

.shk-21b,
.shk-21b *,
.shk-21b *::before,
.shk-21b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-21b {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: linear-gradient(170deg,#12141a,#0c0e12);
  font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}

.shk-21b__panel {
  width: min(460px,100%);
  padding: 24px;
  border-radius: 18px;
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(165,185,220,.14);
  box-shadow: 0 40px 80px -45px rgba(0,0,0,.95);
}

.shk-21b__head {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: rgba(220,230,248,.55);
  padding: 0 6px 12px;
}

.shk-21b__list {
  list-style: none;
  display: grid;
  gap: 8px;
}

.shk-21b__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: rgba(8,10,15,.5);
  border: 1px solid rgba(165,185,220,.12);
  border-radius: 12px;
  transition: opacity .35s,translate .35s,scale .35s;
}

.shk-21b__row.is-going {
  opacity: 0;
  translate: 24px 0;
  scale: .96;
}

.shk-21b__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--c);
}

.shk-21b__name {
  flex: 1;
  font-size: 14px;
  font-weight: 600;
  color: #e6ecf8;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shk-21b__size {
  font-size: 12px;
  color: rgba(220,230,248,.45);
}

.shk-21b__del {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: rgba(220,230,248,.55);
  cursor: pointer;
  transition: background .15s,color .15s;
}

.shk-21b__del svg {
  width: 18px;
  height: 18px;
  overflow: visible;
}

.shk-21b__del:hover {
  background: oklch(0.6 0.21 25 / .15);
  color: oklch(0.78 0.14 25);
}

.shk-21b__lid {
  transform-origin: 12px 5px;
  transform-box: view-box;
}

.shk-21b__del:hover .shk-21b__lid {
  animation: shk-21b-flap .6s ease-in-out infinite;
}

.shk-21b__del:focus-visible {
  outline: 2px solid oklch(0.78 0.14 25);
  outline-offset: 2px;
}

.shk-21b__hint {
  padding: 14px 6px 0;
  font-size: 11.5px;
  color: rgba(220,230,248,.4);
}

@keyframes shk-21b-flap {
  0%,
    100% {
    rotate: 0deg;
    translate: 0 0;
  }

  30% {
    rotate: -14deg;
    translate: -1px -2px;
  }

  60% {
    rotate: -6deg;
    translate: -.5px -1px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-21b__del:hover .shk-21b__lid {
    animation: none;
  }
}
(function(){
  var zone=document.getElementById('shk-21a-zone'),files=document.getElementById('shk-21a-files'),
      label=document.getElementById('shk-21a-label'); if(!zone) return;
  var dragged=null,depth=0,eaten=0;
  files.addEventListener('dragstart',function(e){ dragged=e.target.closest('.shk-21a__chip'); if(e.dataTransfer) e.dataTransfer.effectAllowed='move'; });
  files.addEventListener('dragend',function(){ dragged=null; depth=0; zone.classList.remove('is-hungry'); });
  zone.addEventListener('dragenter',function(){ depth++; zone.classList.add('is-hungry'); });
  zone.addEventListener('dragleave',function(){ if(--depth<=0){ depth=0; zone.classList.remove('is-hungry'); } });
  zone.addEventListener('dragover',function(e){ e.preventDefault(); });      // without this, drop never fires
  zone.addEventListener('animationend',function(){ zone.classList.remove('is-gulping'); });
  zone.addEventListener('drop',function(e){
    e.preventDefault(); depth=0; zone.classList.remove('is-hungry');
    if(!dragged) return;
    dragged.classList.add('is-gone');                                        // chip fades first…
    zone.classList.add('is-gulping');                                        // …then the can gulps
    eaten++; label.textContent=eaten+(eaten===1?' file':' files')+' deleted';
  });
})();

(function(){
  var list=document.getElementById('shk-21b-list'); if(!list) return;
  list.addEventListener('click',function(e){
    var btn=e.target.closest('.shk-21b__del'); if(!btn) return;
    var row=btn.closest('.shk-21b__row');
    row.classList.add('is-going');
    row.addEventListener('transitionend',function once(){ row.remove(); },{once:true});
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Shake Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Trash Icon Shake on Drag Over
Source: https://codefronts.com/motion/css-shake-animation/css-trash-icon-shake-on-drag-over/

The hungriest trash can on the web: drag a file chip over the drop zone and the lid swings open while the can rattles in anticipation; drop, and the chip is swallowed with a gulp squash. Plus a list variant where hovering a row's delete button flaps the lid — a one-hover preview of what the click will do.
## HTML
```html
<div class="shk-21-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-21a" aria-label="Drag files onto a trash can that opens and rattles">
  <div class="shk-21a__desk">
    <div class="shk-21a__files" id="shk-21a-files">
      <div class="shk-21a__chip" draggable="true" tabindex="0">draft-v1.fig</div>
      <div class="shk-21a__chip" draggable="true" tabindex="0">notes-old.md</div>
      <div class="shk-21a__chip" draggable="true" tabindex="0">unused.png</div>
    </div>
    <div class="shk-21a__zone" id="shk-21a-zone" aria-label="Delete drop zone">
      <svg class="shk-21a__can" viewBox="0 0 48 48" aria-hidden="true">
        <g class="shk-21a__lid"><rect x="10" y="10" width="28" height="5" rx="2.5" fill="currentColor"></rect><rect x="19" y="5.5" width="10" height="5" rx="2" fill="currentColor"></rect></g>
        <g class="shk-21a__body"><path d="M13 18h22l-1.8 21a3 3 0 0 1-3 2.8H17.8a3 3 0 0 1-3-2.8z" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linejoin="round"></path><line x1="20" y1="24" x2="20.6" y2="35" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></line><line x1="28" y1="24" x2="27.4" y2="35" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"></line></g>
      </svg>
      <p class="shk-21a__label" id="shk-21a-label">Drag here to delete</p>
    </div>
  </div>
  <p class="shk-21a__hint" aria-hidden="true">lid transitions open (state) · can rattles (anticipation) · drop gulps (payoff)</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-21b" aria-label="List rows whose trash button flaps its lid on hover">
  <div class="shk-21b__panel">
    <p class="shk-21b__head">Uploads</p>
    <ul class="shk-21b__list" id="shk-21b-list">
      <li class="shk-21b__row"><span class="shk-21b__dot" style="--c:oklch(0.75 0.14 250)" aria-hidden="true"></span><span class="shk-21b__name">hero-final-FINAL.webp</span><span class="shk-21b__size">1.2 MB</span><button class="shk-21b__del" type="button" aria-label="Delete hero-final-FINAL.webp"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><g class="shk-21b__lid"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path></g><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></li>
      <li class="shk-21b__row"><span class="shk-21b__dot" style="--c:oklch(0.78 0.14 85)" aria-hidden="true"></span><span class="shk-21b__name">brand-tokens.json</span><span class="shk-21b__size">18 KB</span><button class="shk-21b__del" type="button" aria-label="Delete brand-tokens.json"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><g class="shk-21b__lid"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path></g><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></li>
      <li class="shk-21b__row"><span class="shk-21b__dot" style="--c:oklch(0.72 0.16 340)" aria-hidden="true"></span><span class="shk-21b__name">interview-notes.pdf</span><span class="shk-21b__size">640 KB</span><button class="shk-21b__del" type="button" aria-label="Delete interview-notes.pdf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><g class="shk-21b__lid"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path></g><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button></li>
    </ul>
    <p class="shk-21b__hint" aria-hidden="true">hover a trash button — the lid flaps a preview; click removes the row</p>
  </div>
</section>
</div>
```
## CSS
```css
.shk-21-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.shk-21-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.shk-21a,
.shk-21a *,
.shk-21a *::before,
.shk-21a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-21a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-content: center;
  gap: 22px;
  padding: 48px 24px;
  background: linear-gradient(180deg,#f5f4f1,#e9e7e1);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.shk-21a__desk {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 56px;
}

.shk-21a__files {
  display: grid;
  gap: 12px;
}

.shk-21a__chip {
  padding: 13px 20px;
  background: #fff;
  border: 1.5px solid rgba(32,30,26,.16);
  border-radius: 12px;
  font-size: 14px;
  font-weight: 600;
  color: #201e1a;
  cursor: grab;
  box-shadow: 0 8px 18px -10px rgba(32,30,26,.35);
  transition: opacity .3s,scale .3s,box-shadow .2s;
  user-select: none;
}

.shk-21a__chip:hover {
  box-shadow: 0 12px 22px -10px rgba(32,30,26,.45);
}

.shk-21a__chip:active {
  cursor: grabbing;
}

.shk-21a__chip.is-gone {
  opacity: 0;
  scale: .6;
  pointer-events: none;
}

.shk-21a__zone {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 34px 42px;
  border: 2.5px dashed rgba(32,30,26,.3);
  border-radius: 22px;
  color: #201e1a;
  transition: border-color .25s,background .25s,color .25s;
}

.shk-21a__zone.is-hungry {
  border-color: oklch(0.6 0.21 25);
  background: oklch(0.6 0.21 25 / .07);
  color: oklch(0.52 0.2 25);
}

.shk-21a__zone * {
  pointer-events: none;
}

.shk-21a__can {
  width: 60px;
  height: 60px;
  overflow: visible;
}

.shk-21a__lid {
  transform-origin: 10px 12px;
  transform-box: view-box;
  transition: rotate .28s cubic-bezier(.34,1.4,.64,1),translate .28s cubic-bezier(.34,1.4,.64,1);
}

.shk-21a__zone.is-hungry .shk-21a__lid {
  rotate: -28deg;
  translate: -3px -7px;
}

.shk-21a__zone.is-hungry .shk-21a__body {
  animation: shk-21a-rattle .4s ease-in-out infinite;
}

.shk-21a__zone.is-gulping .shk-21a__can {
  animation: shk-21a-gulp .5s cubic-bezier(.34,1.56,.64,1);
}

.shk-21a__label {
  font-size: 13px;
  font-weight: 700;
}

.shk-21a__hint {
  text-align: center;
  font-size: 12px;
  color: rgba(32,30,26,.5);
}

@keyframes shk-21a-rattle {
  0%,
    100% {
    translate: 0 0;
    rotate: 0deg;
  }

  25% {
    translate: -1.5px 0;
    rotate: -2.5deg;
  }

  75% {
    translate: 1.5px 0;
    rotate: 2.5deg;
  }
}

@keyframes shk-21a-gulp {
  0% {
    scale: 1 1;
  }

  30% {
    scale: 1.15 .82;
  }

  60% {
    scale: .94 1.08;
  }

  100% {
    scale: 1 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-21a__zone.is-hungry .shk-21a__body,
    .shk-21a__zone.is-gulping .shk-21a__can {
    animation: none;
  }
}

.shk-21b,
.shk-21b *,
.shk-21b *::before,
.shk-21b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-21b {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: linear-gradient(170deg,#12141a,#0c0e12);
  font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}

.shk-21b__panel {
  width: min(460px,100%);
  padding: 24px;
  border-radius: 18px;
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(165,185,220,.14);
  box-shadow: 0 40px 80px -45px rgba(0,0,0,.95);
}

.shk-21b__head {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: rgba(220,230,248,.55);
  padding: 0 6px 12px;
}

.shk-21b__list {
  list-style: none;
  display: grid;
  gap: 8px;
}

.shk-21b__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: rgba(8,10,15,.5);
  border: 1px solid rgba(165,185,220,.12);
  border-radius: 12px;
  transition: opacity .35s,translate .35s,scale .35s;
}

.shk-21b__row.is-going {
  opacity: 0;
  translate: 24px 0;
  scale: .96;
}

.shk-21b__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--c);
}

.shk-21b__name {
  flex: 1;
  font-size: 14px;
  font-weight: 600;
  color: #e6ecf8;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shk-21b__size {
  font-size: 12px;
  color: rgba(220,230,248,.45);
}

.shk-21b__del {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: rgba(220,230,248,.55);
  cursor: pointer;
  transition: background .15s,color .15s;
}

.shk-21b__del svg {
  width: 18px;
  height: 18px;
  overflow: visible;
}

.shk-21b__del:hover {
  background: oklch(0.6 0.21 25 / .15);
  color: oklch(0.78 0.14 25);
}

.shk-21b__lid {
  transform-origin: 12px 5px;
  transform-box: view-box;
}

.shk-21b__del:hover .shk-21b__lid {
  animation: shk-21b-flap .6s ease-in-out infinite;
}

.shk-21b__del:focus-visible {
  outline: 2px solid oklch(0.78 0.14 25);
  outline-offset: 2px;
}

.shk-21b__hint {
  padding: 14px 6px 0;
  font-size: 11.5px;
  color: rgba(220,230,248,.4);
}

@keyframes shk-21b-flap {
  0%,
    100% {
    rotate: 0deg;
    translate: 0 0;
  }

  30% {
    rotate: -14deg;
    translate: -1px -2px;
  }

  60% {
    rotate: -6deg;
    translate: -.5px -1px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-21b__del:hover .shk-21b__lid {
    animation: none;
  }
}
```

## JavaScript
```js
(function(){
  var zone=document.getElementById('shk-21a-zone'),files=document.getElementById('shk-21a-files'),
      label=document.getElementById('shk-21a-label'); if(!zone) return;
  var dragged=null,depth=0,eaten=0;
  files.addEventListener('dragstart',function(e){ dragged=e.target.closest('.shk-21a__chip'); if(e.dataTransfer) e.dataTransfer.effectAllowed='move'; });
  files.addEventListener('dragend',function(){ dragged=null; depth=0; zone.classList.remove('is-hungry'); });
  zone.addEventListener('dragenter',function(){ depth++; zone.classList.add('is-hungry'); });
  zone.addEventListener('dragleave',function(){ if(--depth<=0){ depth=0; zone.classList.remove('is-hungry'); } });
  zone.addEventListener('dragover',function(e){ e.preventDefault(); });      // without this, drop never fires
  zone.addEventListener('animationend',function(){ zone.classList.remove('is-gulping'); });
  zone.addEventListener('drop',function(e){
    e.preventDefault(); depth=0; zone.classList.remove('is-hungry');
    if(!dragged) return;
    dragged.classList.add('is-gone');                                        // chip fades first…
    zone.classList.add('is-gulping');                                        // …then the can gulps
    eaten++; label.textContent=eaten+(eaten===1?' file':' files')+' deleted';
  });
})();

(function(){
  var list=document.getElementById('shk-21b-list'); if(!list) return;
  list.addEventListener('click',function(e){
    var btn=e.target.closest('.shk-21b__del'); if(!btn) return;
    var row=btn.closest('.shk-21b__row');
    row.classList.add('is-going');
    row.addEventListener('transitionend',function once(){ row.remove(); },{once:true});
  });
})();
```

How this works

HTML5 drag-and-drop gives you the hooks; CSS does the acting. The dragover/dragleave dance toggles one class:

zone.addEventListener('dragover', e => {
  e.preventDefault();            // required, or drop never fires
  zone.classList.add('is-hungry');
});
zone.addEventListener('dragleave', () =>
  zone.classList.remove('is-hungry'));
.is-hungry .lid{rotate:-28deg;translate:-3px -6px}
.is-hungry .can{animation:rattle .4s ease-in-out infinite}

The lid is a separate SVG group with its hinge as transform-origin — it transitions open (state), while the can animates a rattle loop (anticipation). On drop, a one-shot gulp (squash-stretch) replaces the rattle. Three motions, three jobs: state, anticipation, consummation.

Make it yours

  • Rattle at ±2px/±3° max — the can is excited, not malfunctioning.
  • Open the lid toward the drag source (flip rotate sign by zone side) for an extra-alive feel.
  • On drop, fade the row/chip out FIRST, then gulp — the order sells 'it went in'.
  • Touch has no dragover: pair with a select-then-delete mode or row swipe on mobile.

Gotchas — read before shipping

  • Without e.preventDefault() in dragover, the browser never fires drop — the #1 DnD bug.
  • dragleave fires when crossing CHILD elements; either use a dragenter/leave counter or pointer-events:none on the zone's children.
  • Don't scale the whole zone on hungry state — the geometry change re-triggers enter/leave and the lid flickers.
  • Drag-to-delete must have a keyboard path (delete buttons still work per-row) — the zone is enhancement, not the only door.

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome all modern.

HTML5 DnD is desktop-universal (no touch events — ship a tap fallback). All motion is transform-only SVG group animation.

Techniques used in this demo

Search CodeFronts

Loading…