Bowl

No dependencies
Code format
React + Tailwind
const SWING = 0.95; // one pass, rim to rim
const R = 42; // bowl radius
const BALL = 7.5;
const PIVOT = { x: 50, y: 8 };
const ROLL = R - 1.75 - BALL; // ball centre to bowl centre

const css = `
  .loader-bowl { --ink: light-dark(#373a40, #f1f2f3); --bowl: light-dark(#cbced3, #393c41); display: inline-block; font-size: 116px; width: 1em; height: 0.6em; }
  .loader-bowl svg { display: block; width: 100%; height: 100%; overflow: visible; }
  .loader-bowl__bowl { fill: none; stroke: var(--bowl); stroke-width: 3.5; stroke-linecap: round; }
  /* The ball hangs from the bowl's centre like a pendulum; the two copies behind it lag a little to leave a trail. */
  .loader-bowl__swing { fill: var(--ink); transform-box: view-box; transform-origin: ${PIVOT.x}px ${PIVOT.y}px; animation: loader-bowl-swing ${SWING}s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate; }
  .loader-bowl__swing--1 { opacity: 0.3; animation-delay: 0.055s; }
  .loader-bowl__swing--2 { opacity: 0.12; animation-delay: 0.11s; }
  @keyframes loader-bowl-swing {
    from { transform: rotate(-62deg); }
    to { transform: rotate(62deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .loader-bowl__swing { animation: none; transform: rotate(-30deg); }
    .loader-bowl__swing--1, .loader-bowl__swing--2 { display: none; }
  }
`;

export function Preview() {
  return (
    <>
      <style href="kit-loaders-bowl" precedence="kit">
        {css}
      </style>
      <div className="loader-bowl" role="status" aria-label="Loading">
        <svg viewBox="0 0 100 60" aria-hidden="true">
          <path className="loader-bowl__bowl" d={`M${PIVOT.x - R} ${PIVOT.y} A${R} ${R} 0 0 0 ${PIVOT.x + R} ${PIVOT.y}`} />
          {[2, 1, 0].map((k) => (
            <circle key={k} className={`loader-bowl__swing${k ? ` loader-bowl__swing--${k}` : ''}`} cx={PIVOT.x} cy={PIVOT.y + ROLL} r={BALL} />
          ))}
        </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-bowl
Source on GitHub

About this component

A ball rolls back and forth inside a bowl, slowing at each rim, with a soft trail behind it.

Dependencies
None
Difficulty
Beginner
Triggers
continuous, loop
Techniques
svg, keyframes, transform only
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 ball rests part way up one side.

More loaders

See all
Bubble
Carousel
Chase
Coin