Orbit
A 12 px satellite orbits a fixed 8 px center dot in exactly 1 second — 19 px radius measured, two pseudo-elements, no JavaScript.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
The .loader-orbit container is a 50 × 50 px square with position: relative. Two pseudo-elements, no extra DOM. ::after: an 8 × 8 px disc, #d946ef, centered by left: 50%; top: 50%; transform: translate(-50%, -50%) — it never moves. ::before: a 12 × 12 px disc, #6366f1, anchored at left: 50%; top: 0, with transform-origin: 50% 25px.
The value transform-origin: 50% 25px places the pivot at (6 px, 25 px) in the ::before's local coordinate system: 6 px is half of 12 px (horizontal center of the disc), 25 px is half of 50 px (exact vertical midpoint of the container). The single keyframe to { transform: translateX(-50%) rotate(360deg) } combines two simultaneous effects: translateX(-50%) shifts the orbit center 6 px to the left, bringing the pivot from (31 px, 25 px) to (25 px, 25 px) — the container's geometric center; rotate(360deg) then spins the disc around that point. Resulting radius: 25 − 6 = 19 px, measured constant across 8 successive 125 ms samples.
The 1s linear infinite animation produces a constant 360 °/s, clockwise. No JavaScript, no CSS variables, no element created at load time. The implicit from keyframe value is the transform: translateX(-50%) declared in the element's CSS, which ensures a seamless loop between the starting position (disc at the top, centered) and the continuous orbit.
Accessibility
- prefers-reduced-motion not handled: the animation runs continuously regardless of the system preference. Measured under
reducedMotion: reducein Playwright: the sine component of the transform goes from 0.5430 to −0.5450 over 500 ms — the orbit does not slow down. Add@media (prefers-reduced-motion: reduce) { .loader-orbit::before { animation: none } }to stop the rotation and leave the satellite frozen at its starting position. - For a loading indicator, no semantics are provided:
.loader-orbitis an emptydivwith norole, noaria-busy, noaria-label, and noaria-live. Screen readers ignore both pseudo-elements (CSS-generated content). Add at minimumrole="status"andaria-label="Loading"; if this loader replaces dynamic content, wrap it in anaria-live="polite"region. - The effect works fully on mobile (CSS Animations require no touch interaction): the satellite orbits from page load with no prior gesture. No custom cursor, no mouse events — drop-in compatible with touch contexts.
- Contrast: both discs are UI state indicators, not text (WCAG 3:1 threshold for UI components).
#6366f1on#0a0a0f: 4.4:1 ✓.#d946efon#0a0a0f: 5.3:1 ✓. On a light background, both colors fall below 3:1 — replace#6366f1with#4338caand#d946efwith#a21cafto meet the threshold on white.
Browser compatibility
Pure CSS: @keyframes, transform with translateX() and rotate(), transform-origin, border-radius: 50%, ::before/::after pseudo-elements. No dependencies.
Without CSS Animations (IE 9 and older), ::before renders at its initial state — a #6366f1 disc frozen at the top of the container, horizontally centered. ::after stays centered. No JavaScript to load, no console errors.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="loader-orbit"></div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Container size (CSS — .loader-orbit { width: 50px; height: 50px }) |
50 × 50 px | Rescales the whole system. After a change, recalculate the satellite's transform-origin: its Y value must remain equal to half the new height for the pivot to stay centered. The two disc sizes are independent and adjust separately. |
Orbit radius (CSS — ::before { transform-origin: 50% 25px }) |
25 px arm → 19 px effective radius | The Y value of transform-origin is the rotation arm; effective radius = arm − half satellite height (25 − 6 = 19 px). Lower to 20px for a 14 px radius (tight orbit); raise to 30px for 24 px (resize the container to ≥ 62 × 62 px accordingly). |
Satellite size (CSS — ::before { width: 12px; height: 12px }) |
12 × 12 px | Also adjust transform-origin X: 50% of the new width (e.g. 8px → transform-origin: 4px 25px). Without this adjustment, the satellite orbits slightly off-center. |
Satellite color (CSS — ::before { background: #6366f1 }) |
#6366f1 (indigo) | Independent from the center. Any CSS color value works: gradient (background: linear-gradient(...)), semi-transparent, white. No border or box-shadow in the sold code — add them if a glow is needed. |
Center color (CSS — ::after { background: #d946ef }) |
#d946ef (magenta) | Can also be transparent to remove the center dot and leave only the orbiting satellite, or background: none; border: 2px solid #d946ef; border-radius: 50% for a hollow ring. |
Duration (CSS — animation: 1s linear) |
1 s — 360 °/s | 0.5s: fast orbit, signals intense activity. 2s: slow orbit, suited to long waits or editorial contexts. The linear value keeps angular velocity constant; ease-in-out would produce a perceptible acceleration and deceleration at every pass through the top. |
Center size (CSS — ::after { width: 8px; height: 8px }) |
8 × 8 px | No effect on the satellite's path. A center of 0 px (display: none) gives a lone comet effect. A center of 20 px (40% of the container) reinforces the planet/moon metaphor. |
FAQ
transform-origin, and half the satellite height. The formula is: radius = transform-origin-Y − (satellite-height / 2). For a 60 × 60 px container with a 14 px satellite, set transform-origin: 7px 30px (30 = 60/2, 7 = 14/2): radius = 30 − 7 = 23 px. Never change one value without recalculating the others, or the pivot will drift from the center.conic-gradient ring that rotates — one body, "arc" metaphor. Orbit provides two distinct bodies: a fixed center and a moving satellite — planetary metaphor, legible even at 24 px, with no border thickness or gradient. Choose Orbit when the design calls for a system rather than a circle..loader-orbit) with a different transform-origin and duration. The sold code contains only one satellite (the ::before); adding a second child div with position: absolute and its own rotation animation is the cleanest path — no changes to the existing code.