3DFree

Orbit Circle

A purple core, two dashed rings and three dots going around: a pink one on the inner ring, and on the outer ring a cyan and an amber one moving in opposite directions and crossing each other — pure CSS with offset-path: circle(), zero JavaScript, a still image when the user reduces motion.

CSSoffset-pathOrbit

Updated

Hover or click the scene to interact

This effect is free — the 3D category contains 50 effects total, including 8 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

School portal · Jean-Moulin middle schoolPhysical science · 8th grade

Chapter 3 · Matter

The model of the atom

Carbon: 2 electrons on the 1st shell, 4 on the 2nd.

  • Nucleus: 6 protons, 6 neutrons
  • Electron

① The model of the atom — 8th grade physical science lesson on the school portal

WhenIn the "Matter" chapter, the lesson page shows the carbon atom: a red core marked "C", two electrons on the first shell, four on the second; the legend reads "Carbon: 2 electrons on the 1st shell, 4 on the 2nd".
WhyThe crisp dashed circles are the textbook drawing, and electrons going around at different speeds and in different directions read as independent particles. A still picture does not show that they move.
SettingsDots recolored blue #2563eb, all 9 px; three dots added with the same classes and their own negative delay (-2s on the inner ring, -3.5s and -2.5s on the outer one) to reach 2 + 4 electrons; dark rings on a light background; letter "C" laid over the core; aria-hidden="true" on the root.

Les Varennes leisure park · Map

Around the lake: one direction each

  • Little train, inner track: every 20 min
  • Walkers: clockwise
  • Bikes: counterclockwise

Lakeside path 2.4 km · Bikes at walking pace near the beach

② Around the lake: walkers one way, bikes the other — map of a leisure park

WhenOn the leisure park map, the lake fills the center; the little train runs the inner lap, and on the lakeside path a walker (amber) goes clockwise while a bike (cyan) goes the other way. The legend restates the rule.
WhyThe dot going the wrong way on the same ring states the rule better than an arrow: you see the two flows cross. The original directions (clockwise, and the reverse for cyan) already match the rule.
SettingsCore enlarged to 96 px with a blue gradient to stand for the lake; inner ring as a solid brown line (the track), outer ring as a 5 px beige border (the path); dots enlarged to 16, 13 and 11 px with a white outline, the pink one as a rounded square for the train.

Telenn Glas Celtic circle

Midsummer fest-noz

Beginners' class at 8 pm

  • Inner round
  • Outer round, the other way
  • The leader

③ The two rounds of a Breton dance — page of a Celtic dance club

WhenThe Midsummer fest-noz page announces "Beginners' class at 8 pm"; next to it, around the bonfire, an inner round turns one way and the outer round the other, the leader in amber.
WhyTwo circles turning in opposite directions is the dance figure itself; the effect shows it with no video and without weighing the page down.
SettingsCore recolored as a fire (yellow, orange, red gradient) and enlarged to 46 px with an orange glow; rings in 22% white to stay readable on the night blue; dots in their original colors; scene scaled by 1.15 with transform: scale().

How it works

The .fx-0031-root block is 250 px high and takes the full available width, background rgb(26, 26, 36). In the middle, .mp-orbit-scene (260 × 260 px) stacks two rings of 140 and 210 px (1 px dashed border, white at 10%), a 40 px core with a purple gradient #8b5cf6 → #7c3aed and a glow, and three dots in position: absolute. The core sits in front of the dots (z-index: 2).

The motion takes two declarations per dot. offset-path: circle(70px) draws a 70 px radius circle centered on the scene, which the dot rides by its center, whatever its size (its negative margin makes no difference, measured), and the mpOrbit animation moves offset-distance from 0 to 100%: the dot travels the circle, starting on the right (3 o'clock) and going clockwise. The pink dot (16 px) does the inner lap in 4 s. On the outer ring, circle(105px), the cyan dot (12 px) goes around in 7 s with animation-direction: reverse, so backward, and the amber dot (8 px) in 5 s forward. Their speeds add up as they head toward each other: they cross every 2.9 s, at a different spot each time.

Everything is linear: constant speed, no jolt. No script, no id: two copies of the block on the same page each run on their own (measured in Chromium, Firefox and WebKit). Under prefers-reduced-motion: reduce, a block scoped to .fx-0031-root removes the animations and sets the three dots at 12, 62 and 30% of their ring: they stay visible and spread out instead of piling up at the starting point.

Accessibility

  • Reduced motion: under prefers-reduced-motion: reduce, the three dots stop at fixed places, spread out on their rings. Measured in Chromium, Firefox and WebKit: 0 running animations, 0% of pixels change in 800 ms.
  • Decorative content: the drawing carries no text. When it illustrates a point (an atom, a map), add aria-hidden="true" to the root and write the information next to it, like the legends in the examples; otherwise give the root role="img" and an aria-label that says what is shown.
  • Colors: the rings, white at 10%, are deliberately faint on the dark background. On a light background, darken their border (example 1: rgba(30, 41, 59, .38)), or they vanish and the dots seem to float with no path.
  • No interaction: the effect reacts to neither mouse, finger nor keyboard. Nothing to reach, nothing to miss: it does not get in the way of keyboard navigation on the page.

Browser compatibility

Pure CSS: offset-path: circle(), offset-distance animated with @keyframes, prefers-reduced-motion. No dependency, no JavaScript. Measured in Chromium 151, Firefox 153 and WebKit 26.5: each dot stays within 0.1 px of its ring for the whole lap, same speeds, same stop under reduced motion, two independent copies on the page. circle() as a path arrived later than path(): minimum versions below.

Chrome 116+✓ Full
Firefox 122+✓ Full
Safari✓ Full on WebKit 26.5 (measured); minimum version not checked on a real Safari
Edge 116+✓ Full
Mobile iOS✓ Same as Safari: not checked on a real iPhone
Android Chrome 116+✓ Full

A browser that does not know offset-path: circle() ignores the path: the three dots stay in the center, hidden behind the core (measured by removing the property). You then see a still core and its two rings, with nothing broken.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fx-0031-root">
  <div class="mp-orbit-scene">
    <div class="mp-orbit-ring mp-orbit-ring--1"></div>
    <div class="mp-orbit-ring mp-orbit-ring--2"></div>
    <div class="mp-orbit-center"></div>
    <div class="mp-orbit-dot mp-orbit-dot--1"></div>
    <div class="mp-orbit-dot mp-orbit-dot--2"></div>
    <div class="mp-orbit-dot mp-orbit-dot--3"></div>
  </div>
</div>
CSS
.fx-0031-root,
.fx-0031-root *,
.fx-0031-root *::before,
.fx-0031-root *::after {
  box-sizing: border-box
}

.fx-0031-root {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.mp-orbit-scene {
  width: 260px;
  height: 260px;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

.mp-orbit-center {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: linear-gradient(135deg, #8b5cf6, #7c3aed);
  box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
  z-index: 2;
}

.mp-orbit-ring {
  position: absolute;
  border-radius: 50%;
  border: 1px dashed rgba(255, 255, 255, 0.1);
}

.mp-orbit-ring--1 {
  width: 140px;
  height: 140px;
}

.mp-orbit-ring--2 {
  width: 210px;
  height: 210px;
}

.mp-orbit-dot {
  position: absolute;
  border-radius: 50%;
  top: 50%;
  left: 50%;
}

.mp-orbit-dot--1 {
  width: 16px;
  height: 16px;
  background: #ec4899;
  box-shadow: 0 0 10px rgba(236, 72, 153, 0.5);
  offset-path: circle(70px);
  animation: mpOrbit 4s linear infinite;
  margin: -8px 0 0 -8px;
}

.mp-orbit-dot--2 {
  width: 12px;
  height: 12px;
  background: #06b6d4;
  box-shadow: 0 0 10px rgba(6, 182, 212, 0.5);
  offset-path: circle(105px);
  animation: mpOrbit 7s linear infinite reverse;
  margin: -6px 0 0 -6px;
}

.mp-orbit-dot--3 {
  width: 8px;
  height: 8px;
  background: #f59e0b;
  box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
  offset-path: circle(105px);
  animation: mpOrbit 5s linear infinite;
  margin: -4px 0 0 -4px;
}

@keyframes mpOrbit {
  to {
    offset-distance: 100%;
  }
}

/* Reduced motion: the three dots stop, spread out on their rings (still image). */
@media (prefers-reduced-motion: reduce) {
  .fx-0031-root .mp-orbit-dot { animation: none }
  .fx-0031-root .mp-orbit-dot--1 { offset-distance: 12% }
  .fx-0031-root .mp-orbit-dot--2 { offset-distance: 62% }
  .fx-0031-root .mp-orbit-dot--3 { offset-distance: 30% }
}
JavaScript (fx-0031)

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Orbit radius (CSS — offset-path: circle(70px) and circle(105px)) 70 px and 105 px Also change the size of the matching ring (diameter is twice the radius: 140 and 210 px), or the dot runs beside its line. To enlarge everything at once, prefer transform: scale() on .mp-orbit-scene.
Lap duration (CSS — animation of each .mp-orbit-dot--N) 4 s, 7 s and 5 s A longer duration slows the dot down. Durations with no common multiple (5 and 7) mean the crossings never come back to the same spot.
Direction (CSS — reverse keyword in animation) clockwise, except the cyan dot Add or remove reverse to flip a dot. The start is on the right of the circle (3 o'clock).
Add a dot (HTML + CSS — negative animation-delay) 3 dots Duplicate a .mp-orbit-dot--N and give the copy animation-delay: -2s (half of a 4 s lap): it starts opposite the original, with no wait.
Colors (CSS — background of the core and dots, border of the rings) purple, pink #ec4899, cyan #06b6d4, amber #f59e0b Each dot's glow is a box-shadow in the same hue at 50%: change it along with the color.
Block height (CSS — height of .fx-0031-root) 250px The scene is 260 px and the block trims 5 px at the top and bottom (overflow: hidden) without touching the rings. Below about 220 px, the outer ring's dots get clipped at the top and bottom.

FAQ

How is it different from Multi Orbit (fx-0038)?

Both put dots on circles with offset-path: circle(), but they do not tell the same story. Orbit Circle plays on direction: two dashed rings, three dots, two of which cross head-on on the same ring (opposite flows, dance rounds). Multi Orbit plays on period: three nearly invisible rings, five planets of 6 to 14 px all turning the same way in 3, 5 and 8 s, paired in opposition (shuttles, rounds, a solar system). Pick this one to show two directions of traffic; Multi Orbit to show different rhythms around the same center.

Why offset-path rather than rotating the dot?

The other classic method spins an invisible arm with transform: rotate() and puts the dot at its end: it takes one more element per dot, plus a counter-rotation if the dot carries text or an icon. With offset-path, the dot follows the circle directly, the radius is a single value and the direction changes with reverse. In return, circle() as a path needs at least Chrome 116 or Firefox 122; before that, the dots stay hidden in the center.

Can I put an image or an icon instead of a dot?

Yes: a .mp-orbit-dot can hold an image or an emoji. By default offset-rotate is auto: the element turns to follow the circle's tangent and its content rotates with it. To keep an icon upright, add offset-rotate: 0deg to the dot.