Record
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>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-record" role="status" aria-label="Loading">
<span class="loader-record__disc" aria-hidden="true">
<span class="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"></path>
</defs>
<text>
<textPath href="#loader-record-round" textLength="148" lengthAdjust="spacing">SIDE A · 33⅓ RPM · STEREO ·</textPath>
</text>
<text class="loader-record__title" x="32" y="28">LP</text>
<path class="loader-record__rule" d="M18 38H28M36 38H46"></path>
</svg>
</span>
</span>
<span class="loader-record__gloss" aria-hidden="true"></span>
<svg class="loader-record__arm" viewBox="0 0 230 196" aria-hidden="true">
<circle class="loader-record__base" cx="204" cy="30" r="13"></circle>
<g class="loader-record__swing">
<rect class="loader-record__weight" x="-6" y="-24" width="12" height="14" rx="2.5"></rect>
<path class="loader-record__rod" d="M0 -12V92L-9 104"></path>
<rect class="loader-record__head" x="-17" y="100" width="14" height="18" rx="2" transform="rotate(38 -10 109)"></rect>
<circle cx="0" cy="0" r="4.5" class="loader-record__weight"></circle>
</g>
</svg>
</div>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: translate(204px, 30px) rotate(25.2deg) scale(1); animation: loader-record-arm 7.2s 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: translate(204px, 30px) rotate(25.2deg) scale(1); 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: translate(204px, 30px) rotate(37deg) scale(1); 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: translate(204px, 30px) rotate(37deg) scale(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: translate(204px, 30px) rotate(25.2deg) scale(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: translate(204px, 30px) rotate(25.2deg) scale(1); 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; }
}Goal: Create a "Record" loader.
Visual behavior: A record turning under a fixed shine, its label text going round, while the tonearm creeps inward, lifts and returns.
Technique:
- Pure CSS: every motion is a keyframe animation, so the plain HTML version needs no script.
- The grooves and the label turn together while the conic shine stays still, so the speed shows in the lettering going round.
- The label text is set on a circular textPath with a fixed textLength, so it closes neatly round the spindle.
- The tonearm creeps in across the grooves, lifts (a larger drop shadow and a little scale), swings back and sets down again.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Record" loader.
Visual behavior: A record turning under a fixed shine, its label text going round, while the tonearm creeps inward, lifts and returns.
Technique:
- Pure CSS: every motion is a keyframe animation, so the plain HTML version needs no script.
- The grooves and the label turn together while the conic shine stays still, so the speed shows in the lettering going round.
- The label text is set on a circular textPath with a fixed textLength, so it closes neatly round the spindle.
- The tonearm creeps in across the grooves, lifts (a larger drop shadow and a little scale), swings back and sets down again.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
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.