25 CSS Spinners 03 / 25
Dot Chase Orbit Spinner
Four multi-coloured dots orbit a shared centre, each fading in and out with staggered delays to create a chasing, comet-like orbital trail effect.
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="sp-03">
<div class="sp-03__orbit">
<div class="sp-03__trail"></div>
<div class="sp-03__dot"></div>
<div class="sp-03__dot"></div>
<div class="sp-03__dot"></div>
<div class="sp-03__dot"></div>
</div>
</div> <div class="sp-03">
<div class="sp-03__orbit">
<div class="sp-03__trail"></div>
<div class="sp-03__dot"></div>
<div class="sp-03__dot"></div>
<div class="sp-03__dot"></div>
<div class="sp-03__dot"></div>
</div>
</div>.sp-03,.sp-03 *,.sp-03 *::before,.sp-03 *::after{box-sizing:border-box;margin:0;padding:0}
.sp-03{
--bg:#0b1120;
--d1:#f9a825;
--d2:#ff6b35;
--d3:#e91e8c;
--d4:#7c4dff;
display:flex;
align-items:center;
justify-content:center;
min-height:100vh;
background:var(--bg);
}
.sp-03__orbit{
position:relative;
width:72px;
height:72px;
animation:sp-03-spin 1.4s linear infinite;
}
.sp-03__dot{
position:absolute;
width:12px;
height:12px;
border-radius:50%;
top:50%;
left:50%;
transform-origin:0 0;
}
.sp-03__dot:nth-child(1){
background:var(--d1);
box-shadow:0 0 8px var(--d1);
transform:rotate(0deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:0s;
}
.sp-03__dot:nth-child(2){
background:var(--d2);
box-shadow:0 0 8px var(--d2);
transform:rotate(90deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:-0.35s;
}
.sp-03__dot:nth-child(3){
background:var(--d3);
box-shadow:0 0 8px var(--d3);
transform:rotate(180deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:-0.7s;
}
.sp-03__dot:nth-child(4){
background:var(--d4);
box-shadow:0 0 8px var(--d4);
transform:rotate(270deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:-1.05s;
}
.sp-03__trail{
position:absolute;
inset:8px;
border-radius:50%;
border:1px solid rgba(255,255,255,0.06);
}
@keyframes sp-03-spin{
to{transform:rotate(360deg)}
}
@keyframes sp-03-fade{
0%,100%{opacity:1;transform-origin:0 0}
50%{opacity:0.25}
}
@media (prefers-reduced-motion: reduce){
.sp-03__orbit,.sp-03__dot{animation:none}
} .sp-03,.sp-03 *,.sp-03 *::before,.sp-03 *::after{box-sizing:border-box;margin:0;padding:0}
.sp-03{
--bg:#0b1120;
--d1:#f9a825;
--d2:#ff6b35;
--d3:#e91e8c;
--d4:#7c4dff;
display:flex;
align-items:center;
justify-content:center;
min-height:100vh;
background:var(--bg);
}
.sp-03__orbit{
position:relative;
width:72px;
height:72px;
animation:sp-03-spin 1.4s linear infinite;
}
.sp-03__dot{
position:absolute;
width:12px;
height:12px;
border-radius:50%;
top:50%;
left:50%;
transform-origin:0 0;
}
.sp-03__dot:nth-child(1){
background:var(--d1);
box-shadow:0 0 8px var(--d1);
transform:rotate(0deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:0s;
}
.sp-03__dot:nth-child(2){
background:var(--d2);
box-shadow:0 0 8px var(--d2);
transform:rotate(90deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:-0.35s;
}
.sp-03__dot:nth-child(3){
background:var(--d3);
box-shadow:0 0 8px var(--d3);
transform:rotate(180deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:-0.7s;
}
.sp-03__dot:nth-child(4){
background:var(--d4);
box-shadow:0 0 8px var(--d4);
transform:rotate(270deg) translateX(28px) translateY(-6px);
animation:sp-03-fade 1.4s linear infinite;
animation-delay:-1.05s;
}
.sp-03__trail{
position:absolute;
inset:8px;
border-radius:50%;
border:1px solid rgba(255,255,255,0.06);
}
@keyframes sp-03-spin{
to{transform:rotate(360deg)}
}
@keyframes sp-03-fade{
0%,100%{opacity:1;transform-origin:0 0}
50%{opacity:0.25}
}
@media (prefers-reduced-motion: reduce){
.sp-03__orbit,.sp-03__dot{animation:none}
}How this works
Each dot is absolutely positioned at the centre (top:50%;left:50%) with transform-origin:0 0, then pushed outward with translateX(28px) after a fixed rotate(Ndeg) — so all four sit at 0°, 90°, 180°, and 270° on the same orbit radius. The parent .sp-03__orbit div spins continuously with sp-03-spin, carrying the dots with it.
A second separate sp-03-fade keyframe on each dot drives opacity between 1 and 0.25, offset by animation-delay in 0.35 s steps, so whichever dot is at the "front" position appears brightest, simulating a directional chase trail.
Customize
- Edit
--d1through--d4to change the four dot colours independently. - Increase the orbit radius from
translateX(28px)to36pxfor a wider sweep around a larger central element. - Add a fifth dot at
rotate(216deg)and setanimation-delay:-1.12sto extend the chase to a pentagon orbit. - Change dot size from
12pxto8pxwith a longer trail delay (0.45ssteps) for a tighter comet appearance. - Adjust the parent spin duration (default
1.4s) to control overall speed — the fade delays scale automatically since they are set in absolute seconds.
Watch out for
- The
transform-origin:0 0trick means that changing the dot'swidthorheightshifts its radial distance — always re-check orbit radius after resizing dots. - Negative
animation-delayvalues are used for the fade — this is valid per the spec but Safari below v14 occasionally mis-renders the first frame; add a tinyanimation-fill-mode:backwardsas a workaround. - Rotating the parent while each child also animates opacity can compound GPU layer creation — keep the child opacity animation on
will-change:opacityif you see compositing budget warnings.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 60+ | 12+ | 60+ | 60+ |
Uses only transform and opacity animations; compositor-friendly on all modern browsers.