AI CSS Animations

Cookie Bite Loader

A chocolate chip cookie loses three bites, crumbs dropping each time, then a fresh one pops back in. Bites are mask circles whose radius grows through @property variables.

Also known as: cookie eating animation, cookie loader css, cookie consent loader.

HTML

<div class="ldr-cookie" role="status" aria-label="Loading">
  <span class="ldr-cookie-cookie"></span>
  <span class="ldr-cookie-crumb"></span><span class="ldr-cookie-crumb"></span><span class="ldr-cookie-crumb"></span>
</div>

CSS

@property --ldr-cookie-a { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --ldr-cookie-b { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --ldr-cookie-c { syntax: "<length>"; inherits: false; initial-value: 0px; }
.ldr-cookie {
  --s: var(--ldr-size, 48px);
  --c: var(--ldr-color, #3b82f6);
  --chip: color-mix(in srgb, var(--c) 40%, #000);
  position: relative;
  width: var(--s);
  height: var(--s);
}
.ldr-cookie-cookie {
  --bite: calc(var(--s) * 0.2);
  position: absolute;
  inset: 8%;
  border-radius: 50%;
  background:
    radial-gradient(circle calc(var(--s) * 0.045) at 30% 28%, var(--chip) 92%, transparent),
    radial-gradient(circle calc(var(--s) * 0.038) at 56% 36%, var(--chip) 92%, transparent),
    radial-gradient(circle calc(var(--s) * 0.04) at 24% 55%, var(--chip) 92%, transparent),
    radial-gradient(circle calc(var(--s) * 0.045) at 46% 62%, var(--chip) 92%, transparent),
    radial-gradient(circle calc(var(--s) * 0.038) at 70% 60%, var(--chip) 92%, transparent),
    radial-gradient(circle calc(var(--s) * 0.035) at 50% 84%, var(--chip) 92%, transparent),
    radial-gradient(circle calc(var(--s) * 0.032) at 76% 40%, var(--chip) 92%, transparent),
    var(--c);
  -webkit-mask:
    radial-gradient(circle var(--ldr-cookie-a) at 91% 9%, transparent 96%, #000),
    radial-gradient(circle calc(var(--ldr-cookie-a) * 0.7) at 102% 26%, transparent 96%, #000),
    radial-gradient(circle var(--ldr-cookie-b) at 98% 78%, transparent 96%, #000),
    radial-gradient(circle calc(var(--ldr-cookie-b) * 0.7) at 89% 93%, transparent 96%, #000),
    radial-gradient(circle var(--ldr-cookie-c) at 30% 102%, transparent 96%, #000),
    radial-gradient(circle calc(var(--ldr-cookie-c) * 0.7) at 14% 94%, transparent 96%, #000);
  -webkit-mask-composite: source-in;
  mask:
    radial-gradient(circle var(--ldr-cookie-a) at 91% 9%, transparent 96%, #000),
    radial-gradient(circle calc(var(--ldr-cookie-a) * 0.7) at 102% 26%, transparent 96%, #000),
    radial-gradient(circle var(--ldr-cookie-b) at 98% 78%, transparent 96%, #000),
    radial-gradient(circle calc(var(--ldr-cookie-b) * 0.7) at 89% 93%, transparent 96%, #000),
    radial-gradient(circle var(--ldr-cookie-c) at 30% 102%, transparent 96%, #000),
    radial-gradient(circle calc(var(--ldr-cookie-c) * 0.7) at 14% 94%, transparent 96%, #000);
  mask-composite: intersect;
  animation: ldr-cookie-eat 3s infinite;
}
.ldr-cookie-crumb {
  position: absolute;
  left: 76%;
  top: 30%;
  width: calc(var(--s) * 0.06);
  height: calc(var(--s) * 0.06);
  border-radius: 30%;
  background: var(--c);
  opacity: 0;
  animation: ldr-cookie-crumb 3s 0.5s infinite;
}
.ldr-cookie-crumb:nth-child(3) {
  left: 86%;
  top: 72%;
  animation-delay: 1.22s;
}
.ldr-cookie-crumb:nth-child(4) {
  left: 30%;
  top: 88%;
  animation-delay: 1.94s;
}
@keyframes ldr-cookie-eat {
  0%, 14%  { --ldr-cookie-a: 0px; transform: scale(1) rotate(0); opacity: 1; }
  16%      { transform: scale(0.94) rotate(-6deg); }
  18%      { --ldr-cookie-a: var(--bite); }
  21%, 38% { --ldr-cookie-b: 0px; transform: scale(1) rotate(0); }
  40%      { transform: scale(0.94) rotate(-6deg); }
  42%      { --ldr-cookie-b: var(--bite); }
  45%, 62% { --ldr-cookie-c: 0px; transform: scale(1) rotate(0); }
  64%      { transform: scale(0.94) rotate(-6deg); }
  66%      { --ldr-cookie-c: var(--bite); }
  69%, 80% { transform: scale(1) rotate(0); opacity: 1; }
  86%      { --ldr-cookie-a: var(--bite); --ldr-cookie-b: var(--bite); --ldr-cookie-c: var(--bite); transform: scale(0.5); opacity: 0; }
  87%, 92% { --ldr-cookie-a: 0px; --ldr-cookie-b: 0px; --ldr-cookie-c: 0px; transform: scale(0.5); opacity: 0; animation-timing-function: cubic-bezier(0.3, 1.6, 0.6, 1); }
  100%     { transform: scale(1); opacity: 1; }
}
@keyframes ldr-cookie-crumb {
  0%        { transform: translateY(0) rotate(0); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  20%, 100% { transform: translateY(calc(var(--s) * 0.2)) rotate(90deg); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ldr-cookie, .ldr-cookie *, .ldr-cookie::before, .ldr-cookie::after, .ldr-cookie *::before, .ldr-cookie *::after {
    animation-duration: 3s;
  }
}

Customizing this loader

  • Color: set --ldr-color on the element or a parent. Currently previewing #3b82f6.
  • Size: set --ldr-size. Every dimension in the snippet is derived from it. Currently previewing 64px.
  • Speed: change the duration on the animation shorthand. Delays on the children are relative, so they keep their rhythm.
  • Accessibility: the root carries role="status" and an aria-label so screen readers announce the wait. The reduced-motion rule at the end slows the animation for users who asked for less motion.

Similar loaders