Vortex
React + Tailwind
const css = `
@keyframes loader-vortex-fall {
0% { transform: rotate(var(--a)) translateX(50px) scale(1); opacity: 0; }
15% { opacity: 1; }
100% { transform: rotate(calc(var(--a) + 600deg)) translateX(0) scale(0.2); opacity: 0.2; }
}
@keyframes loader-vortex-core {
0%, 100% { transform: scale(0.85); opacity: 0.7; }
50% { transform: scale(1.15); opacity: 1; }
}
.loader-vortex { position: relative; width: 108px; height: 108px; }
.loader-vortex__p {
position: absolute;
left: 50%;
top: 50%;
width: 4px;
height: 4px;
margin: -2px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
opacity: 0;
/* ease-in: slow at the rim, whipping round faster as it falls in. */
animation: loader-vortex-fall 1.8s cubic-bezier(0.55, 0, 0.8, 0.5) var(--d) infinite;
}
.loader-vortex__core {
position: absolute;
left: 50%;
top: 50%;
width: 12px;
height: 12px;
margin: -6px;
border-radius: 50%;
background: light-dark(#fbfcfd, #121417);
box-shadow: 0 0 0 1.5px light-dark(#42454c, #e5e6e9), 0 0 14px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent);
animation: loader-vortex-core 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.loader-vortex__p { animation-duration: 6s; }
.loader-vortex__core { animation: none; }
}
`;
const COUNT = 24;
export function VortexLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-vortex" role="status" aria-label="Loading">
{Array.from({ length: COUNT }, (_, i) => (
<span
key={i}
className="loader-vortex__p"
aria-hidden="true"
style={
{
'--a': `${((i * 137.5) % 360).toFixed(1)}deg`,
'--d': `${(-(i / COUNT) * 1.8).toFixed(3)}s`,
} as React.CSSProperties
}
/>
))}
<span className="loader-vortex__core" aria-hidden="true" />
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-vortex" role="status" aria-label="Loading">
<span class="loader-vortex__p" aria-hidden="true" style="--a:0.0deg;--d:0.000s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:137.5deg;--d:-0.075s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:275.0deg;--d:-0.150s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:52.5deg;--d:-0.225s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:190.0deg;--d:-0.300s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:327.5deg;--d:-0.375s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:105.0deg;--d:-0.450s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:242.5deg;--d:-0.525s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:20.0deg;--d:-0.600s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:157.5deg;--d:-0.675s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:295.0deg;--d:-0.750s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:72.5deg;--d:-0.825s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:210.0deg;--d:-0.900s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:347.5deg;--d:-0.975s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:125.0deg;--d:-1.050s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:262.5deg;--d:-1.125s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:40.0deg;--d:-1.200s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:177.5deg;--d:-1.275s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:315.0deg;--d:-1.350s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:92.5deg;--d:-1.425s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:230.0deg;--d:-1.500s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:7.5deg;--d:-1.575s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:145.0deg;--d:-1.650s"></span>
<span class="loader-vortex__p" aria-hidden="true" style="--a:282.5deg;--d:-1.725s"></span>
<span class="loader-vortex__core" aria-hidden="true"></span>
</div>CSS
@keyframes loader-vortex-fall {
0% { transform: rotate(var(--a)) translateX(50px) scale(1); opacity: 0; }
15% { opacity: 1; }
100% { transform: rotate(calc(var(--a) + 600deg)) translateX(0) scale(0.2); opacity: 0.2; }
}
@keyframes loader-vortex-core {
0%, 100% { transform: scale(0.85); opacity: 0.7; }
50% { transform: scale(1.15); opacity: 1; }
}
.loader-vortex { position: relative; width: 108px; height: 108px; }
.loader-vortex__p {
position: absolute;
left: 50%;
top: 50%;
width: 4px;
height: 4px;
margin: -2px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
opacity: 0;
/* ease-in: slow at the rim, whipping round faster as it falls in. */
animation: loader-vortex-fall 1.8s cubic-bezier(0.55, 0, 0.8, 0.5) var(--d) infinite;
}
.loader-vortex__core {
position: absolute;
left: 50%;
top: 50%;
width: 12px;
height: 12px;
margin: -6px;
border-radius: 50%;
background: light-dark(#fbfcfd, #121417);
box-shadow: 0 0 0 1.5px light-dark(#42454c, #e5e6e9), 0 0 14px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent);
animation: loader-vortex-core 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.loader-vortex__p { animation-duration: 6s; }
.loader-vortex__core { animation: none; }
}Goal: Create a "Vortex" loader.
Visual behavior: Particles spiral in from the rim of a vortex, slow at the edge and whipping round faster as they fall toward a pulsing dark core.
Technique:
- Twenty-four dots start at golden-angle bearings (137.5deg apart) so they never bunch; each has a negative delay so the stream is already flowing.
- One keyframe takes each dot from rotate(a) translateX(50px) to rotate(a + 600deg) translateX(0) scale(0.2): turning while moving inward makes the spiral.
- An ease-in curve keeps them slow at the rim and fast near the centre; the core breathes on its own scale keyframe.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the particles slow to a 6s fall and the core stops pulsing.
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 "Vortex" loader.
Visual behavior: Particles spiral in from the rim of a vortex, slow at the edge and whipping round faster as they fall toward a pulsing dark core.
Technique:
- Twenty-four dots start at golden-angle bearings (137.5deg apart) so they never bunch; each has a negative delay so the stream is already flowing.
- One keyframe takes each dot from rotate(a) translateX(50px) to rotate(a + 600deg) translateX(0) scale(0.2): turning while moving inward makes the spiral.
- An ease-in curve keeps them slow at the rim and fast near the centre; the core breathes on its own scale keyframe.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the particles slow to a 6s fall and the core stops pulsing.
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
Particles spiral in from the rim of a vortex, slow at the edge and whipping round faster as they fall toward a pulsing dark core.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- continuous, loop, delay stagger, glow
- Techniques
- keyframes, css variables, stagger
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the particles slow to a 6s fall and the core stops pulsing.