Record

No dependencies
Code format
React + Tailwind
const T = 7.2; // one pass of the tonearm
const PIVOT = { x: 204, y: 30 };
const REST = 25.2; // degrees: the stylus on the lead-in groove
const IN = 37; // degrees: as far in as it plays before lifting
const arm = (deg: number, s = 1) => `translate(${PIVOT.x}px, ${PIVOT.y}px) rotate(${deg}deg) scale(${s})`;
const LABEL = 'SIDE A · 33⅓ RPM · STEREO · ';

const css = `
  .loader-record { --vinyl: light-dark(#42454c, #1a1c1f); --groove: color-mix(in oklab, var(--vinyl) 82%, light-dark(#6d7179, #7d8087)); --label: light-dark(#ebedf0, #c6c8cd); --print: light-dark(#373a40, #1a1c1f); --gloss: light-dark(#fbfcfd, #f1f2f3); --metal: light-dark(#6d7179, #a7abb2); position: relative; width: 230px; height: 196px; }
  .loader-record__disc { position: absolute; left: 4px; top: 12px; width: 172px; height: 172px; border-radius: 50%; background: radial-gradient(circle, transparent 0 33px, var(--vinyl) 33px 36px, transparent 36px 57px, var(--vinyl) 57px 58.5px, transparent 58.5px 71px, var(--vinyl) 71px 72px, transparent 72px 84px, var(--vinyl) 84px), repeating-radial-gradient(circle, var(--vinyl) 0 1.5px, var(--groove) 1.7px 2.2px); box-shadow: 0 0 0 1px color-mix(in oklab, light-dark(#6d7179, #7d8087) 40%, transparent), 0 10px 18px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); animation: loader-record-turn 1.8s linear infinite; }
  /* The label turns with the record, so its lettering shows the speed. */
  .loader-record__label { position: absolute; left: 50%; top: 50%; width: 64px; height: 64px; margin: -32px; border-radius: 50%; background: radial-gradient(circle, var(--vinyl) 0 2.6px, var(--label) 3px); }
  .loader-record__label svg { width: 100%; height: 100%; overflow: visible; }
  .loader-record__label text { fill: var(--print); font: 600 5.6px/1 'Helvetica Neue', Arial, sans-serif; letter-spacing: 0.06em; }
  .loader-record__rule { fill: none; stroke: var(--print); stroke-width: 0.8; }
  .loader-record__label .loader-record__title { font: italic 700 11px/1 Georgia, 'Times New Roman', serif; letter-spacing: 0; text-anchor: middle; }
  /* The shine stays put while the grooves run under it. */
  .loader-record__gloss { position: absolute; left: 4px; top: 12px; width: 172px; height: 172px; border-radius: 50%; background: conic-gradient(from 20deg, transparent 0deg, color-mix(in oklab, var(--gloss) 16%, transparent) 22deg, transparent 44deg 180deg, color-mix(in oklab, var(--gloss) 11%, transparent) 202deg, transparent 224deg); -webkit-mask: radial-gradient(circle, transparent 0 34px, #000 35px); mask: radial-gradient(circle, transparent 0 34px, #000 35px); pointer-events: none; }
  .loader-record__arm { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
  .loader-record__base { fill: color-mix(in oklab, var(--metal) 60%, light-dark(#ebedf0, #232529)); stroke: var(--metal); stroke-width: 1.5; }
  .loader-record__swing { transform-box: view-box; transform-origin: 0 0; transform: ${arm(REST)}; animation: loader-record-arm ${T}s infinite; }
  .loader-record__rod { fill: none; stroke: var(--metal); stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round; }
  .loader-record__weight { fill: color-mix(in oklab, var(--metal) 70%, light-dark(#fbfcfd, #121417)); }
  .loader-record__head { fill: light-dark(#50535b, #393c41); stroke: var(--metal); stroke-width: 1; }
  @keyframes loader-record-turn { to { transform: rotate(1turn); } }
  @keyframes loader-record-arm {
    0% { transform: ${arm(REST)}; filter: drop-shadow(0 2px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); animation-timing-function: linear; }
    78% { transform: ${arm(IN)}; filter: drop-shadow(0 2px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); animation-timing-function: ease-out; }
    82% { transform: ${arm(IN, 1.04)}; filter: drop-shadow(4px 9px 5px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3))); animation-timing-function: ease-in-out; }
    93% { transform: ${arm(REST, 1.04)}; filter: drop-shadow(4px 9px 5px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3))); animation-timing-function: ease-in; }
    98%, 100% { transform: ${arm(REST)}; filter: drop-shadow(0 2px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
  }
  @media (prefers-reduced-motion: reduce) {
    .loader-record * { animation: none !important; }
  }
`;

export function Preview() {
  return (
    <>
      <style href="kit-loaders-record" precedence="kit">
        {css}
      </style>
      <div className="loader-record" role="status" aria-label="Loading">
        <span className="loader-record__disc" aria-hidden="true">
          <span className="loader-record__label">
            <svg viewBox="0 0 64 64">
              <defs>
                <path id="loader-record-round" d="M32 32m-24 0a24 24 0 1 1 48 0a24 24 0 1 1-48 0" />
              </defs>
              <text>
                <textPath href="#loader-record-round" textLength={148} lengthAdjust="spacing">
                  {LABEL}
                </textPath>
              </text>
              <text className="loader-record__title" x={32} y={28}>
                LP
              </text>
              <path className="loader-record__rule" d="M18 38H28M36 38H46" />
            </svg>
          </span>
        </span>
        <span className="loader-record__gloss" aria-hidden="true" />
        <svg className="loader-record__arm" viewBox="0 0 230 196" aria-hidden="true">
          <circle className="loader-record__base" cx={PIVOT.x} cy={PIVOT.y} r={13} />
          <g className="loader-record__swing">
            <rect className="loader-record__weight" x={-6} y={-24} width={12} height={14} rx={2.5} />
            <path className="loader-record__rod" d="M0 -12V92L-9 104" />
            <rect className="loader-record__head" x={-17} y={100} width={14} height={18} rx={2} transform="rotate(38 -10 109)" />
            <circle cx={0} cy={0} r={4.5} className="loader-record__weight" />
          </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-record
Source on GitHub

About this component

A record turning under a fixed shine, its label text going round, while the tonearm creeps inward, lifts and returns.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop
Techniques
keyframes, svg, conic gradient, filter
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 record and tonearm rest in place.

More loaders

See all
Relay
Shuffle
Sine
Snow Globe