Home / Snippets / Animation /

Orbit path

Make an element orbit in a perfect circle using offset-path: circle() — no nested transform hacks, no JavaScript, just clean CSS.

New feature
animationno-js

Quick implementation

.orbit-container {
  position: relative;
  /* Size your container */
}

.orbit-planet {
  position: absolute;
  top: 50%; left: 50%;
  offset-path: circle(4rem at 50% 50%); /* radius, centered in container */
  offset-distance: 0%;
  animation: orbit 3s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
  .orbit-planet { animation: none; }
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building CSS motion path animations.

Goal: Make one or more elements orbit in perfect circles around a center
point using CSS offset-path: circle() — no nested transform tricks, no
JavaScript.

Technical constraints:
- Position each planet as position: absolute; top: 50%; left: 50% inside
  a position: relative container.
- Use offset-path: circle(Xrem at 50% 50%) where X is the orbit radius and
  "at 50% 50%" anchors the circle center to the center of the containing
  block (the orbit container).
- Animate offset-distance from 0% to 100% with @keyframes.
- Use different animation-duration values for different orbit speeds.
- Use animation-direction: reverse on some planets for counter-clockwise
  orbits.
- Use oklch() for all color values — no hex or rgba.
- Mark the entire orbit animation container aria-hidden="true" if purely
  decorative.
- Wrap the animation in @media (prefers-reduced-motion: reduce) and stop it.

Framework variant (pick one):
A) Vanilla CSS — .orbit-container and .orbit-planet classes with
   CSS custom properties for --orbit-radius and --orbit-duration.
B) React component — accepts an array of planet objects (radius, duration,
   color, reverse) and renders the system.

Edge cases to handle:
- If you want the planet to face its direction of travel (e.g. an arrow),
  add offset-rotate: auto — otherwise omit it so the planet stays upright.
- For many simultaneous orbits, add will-change: offset-distance to promote
  each planet to its own compositor layer upfront and avoid layer promotion
  jank on the first frame.
- The circle() at position matters: "at 50% 50%" places the orbit center at
  the center of the containing block — this is what you want. "at 0 0" would
  orbit around the top-left corner of the container instead.

Return CSS only (or a React component if variant B).

Why offset-path: circle() beats the nested transform trick

Before offset-path: circle(), CSS orbits required a well-known but awkward technique: nest a planet inside a wrapper element, apply rotate() to the wrapper to move it around the center, then counter-rotate the planet itself by the same amount to keep it upright. This two-element trick is hard to read, breaks if you add transform for other reasons, and becomes increasingly messy when you need multiple orbits at different radii.

The circle() shape function makes circular motion paths first-class in CSS. One element, one offset-path declaration, one @keyframes. Adding a second planet at a different radius or speed is a single class override — no additional wrapper needed.

Understanding circle(radius at x y)

offset-path: circle(4rem at 50% 50%) defines a circle of radius 4rem centered at 50% 50% of the containing block — the exact center of .orbit-system. The path coordinates are always in the containing block's coordinate space, so 50% 50% resolves to the center regardless of where the planet element itself is positioned.

Animating offset-distance from 0% to 100% traces the full circumference. Multiple planets use different radii in their circle() values and different animation-duration values for staggered, visually distinct orbits. Setting animation-direction: reverse on a planet makes it orbit counter-clockwise, adding depth to the composition.

Because offset-rotate is not set here, the planets remain upright as they travel. Add offset-rotate: auto if you want the planet to face its direction of travel — useful for arrow or chevron shapes.

Accessibility and performance

Set aria-hidden="true" on the orbit container when it is purely decorative — an orbiting spinner adds no semantic information. offset-distance is GPU-composited, so all planets animate at 60 fps with zero layout recalculation, regardless of how many orbits are active.

For many simultaneous orbits, consider will-change: offset-distance to promote each planet to its own compositor layer upfront and avoid a brief layer-promotion jank on the first animation frame. Always honour prefers-reduced-motion — continuous circular motion is among the most disorienting patterns for users with vestibular disorders.