Goo

No dependencies
Code format
React + Tailwind
import { useId } from 'react';

const T = 2;

// Two discs share one spot and pull apart. A blur plus a hard alpha cut (the
// filter below) melts them into one shape while they are close, so the neck
// between them stretches and snaps like a drop of liquid.
const css = `
  .loader-goo { --ink: light-dark(#373a40, #f1f2f3); display: inline-block; font-size: 112px; width: 1em; height: 1em; }
  .loader-goo svg { display: block; width: 100%; height: 100%; }
  .loader-goo__turn { transform-box: view-box; transform-origin: 50px 50px; animation: loader-goo-turn ${T}s cubic-bezier(0.65, 0, 0.35, 1) infinite; }
  .loader-goo__blob { fill: var(--ink); transform-box: view-box; transform-origin: 50px 50px; animation: loader-goo-split ${T}s infinite; }
  .loader-goo__blob + .loader-goo__blob { --dir: 1; }
  @keyframes loader-goo-split {
    0% { transform: translateX(0) scale(1); animation-timing-function: cubic-bezier(0.5, 0, 0.3, 1); }
    45%, 55% { transform: translateX(calc(var(--dir, -1) * 21px)) scale(0.74); animation-timing-function: cubic-bezier(0.6, 0, 0.35, 1); }
    100% { transform: translateX(0) scale(1); }
  }
  @keyframes loader-goo-turn { to { transform: rotate(180deg); } }
  @media (prefers-reduced-motion: reduce) {
    .loader-goo__turn, .loader-goo__blob { animation: none; }
    .loader-goo__blob { transform: translateX(calc(var(--dir, -1) * 12px)) scale(0.82); }
  }
`;

export function Preview() {
  const id = useId();
  return (
    <>
      <style href="kit-loaders-goo" precedence="kit">
        {css}
      </style>
      <div className="loader-goo" role="status" aria-label="Loading">
        <svg viewBox="0 0 100 100" aria-hidden="true">
          <defs>
            <filter id={id} x="0" y="0" width="100" height="100" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
              <feGaussianBlur stdDeviation="5" />
              <feColorMatrix values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7" />
            </filter>
          </defs>
          <g filter={`url(#${id})`}>
            <g className="loader-goo__turn">
              <circle className="loader-goo__blob" cx="50" cy="50" r="17" />
              <circle className="loader-goo__blob" cx="50" cy="50" r="17" />
            </g>
          </g>
        </svg>
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/loader-goo
Source on GitHub

About this component

A drop of goo pinches into two, spins a half turn while they stretch apart, and pulls back into one.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop, morph
Techniques
svg, svg filter, keyframes
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion the drop shows half pinched, still joined.

More loaders

See all
Marble Run
Pencil
Quarters
Record