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
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.
Updated
This effect is free — the 3D category contains 50 effects total, including 8 free. Effect.Labs has 840 vanilla effects. Explore the category →
Chapter 3 · Matter
Carbon: 2 electrons on the 1st shell, 4 on the 2nd.
Les Varennes leisure park · Map
Lakeside path 2.4 km · Bikes at walking pace near the beach
Telenn Glas Celtic circle
Beginners' class at 8 pm
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.
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.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.rgba(30, 41, 59, .38)), or they vanish and the dots seem to float with no path.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.
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.
Copy the three blocks into your page. No dependencies.
<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>
.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% }
}
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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. |
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.
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.
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.