26 CSS Circular Menus17 / 26

Pure CSSMIT licensed

Single Satellite Orbiting Menu Toggle

One satellite button endlessly circles a planet-sized CTA, riding a CSS Motion Path — offset-path: circle() — with no keyframe math at all. Hover the system and the satellite eases to a stop at 12 o'clock, ready to click. A striking attention magnet for a single secondary action: 'What's new', a chat bubble, a promo.

Published Updated

Live Demo
Try it

The code

<section class="ccm-17" aria-label="Orbiting satellite toggle demo">
  <div class="ccm-17__stage">
    <div class="ccm-17__system">
      <span class="ccm-17__track" aria-hidden="true"></span>
      <button type="button" class="ccm-17__planet">Start free trial</button>
      <a class="ccm-17__sat" href="#whats-new"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4l1.8 4.6L18.5 10l-4.7 1.4L12 16l-1.8-4.6L5.5 10l4.7-1.4z"/></svg><span class="ccm-17__sr">What's new</span></a>
    </div>
    <p class="ccm-17__note">offset-path: circle() · hover to dock the satellite</p>
  </div>
</section>
.ccm-17,
.ccm-17 *,
.ccm-17 *::before,
.ccm-17 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccm-17 {
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 40%,oklch(0.3 0.05 280),oklch(0.19 0.035 290) 75%);
  --brand: oklch(0.62 0.19 265);
  --star: oklch(0.8 0.16 85);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.ccm-17__stage {
  width: 100%;
  height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  border-radius: 20px;
  background: radial-gradient(circle at 50% 40%,oklch(0.3 0.05 280),oklch(0.19 0.035 290) 75%);
  overflow: hidden;
}

.ccm-17__system {
  position: relative;
  width: 240px;
  height: 240px;
  display: grid;
  place-items: center;
}

.ccm-17__track {
  position: absolute;
  inset: 28px;
  border-radius: 50%;
  border: 1px dashed oklch(0.65 0.08 280 / .35);
}

.ccm-17__planet {
  padding: 20px 30px;
  border: none;
  border-radius: 999px;
  background: linear-gradient(140deg,var(--brand),oklch(0.52 0.21 290));
  color: #fff;
  font: 700 16px/1 'Segoe UI',system-ui,sans-serif;
  letter-spacing: .01em;
  cursor: pointer;
  box-shadow: 0 18px 44px -14px oklch(0.5 0.2 275 / .8),inset 0 1px 0 oklch(1 0 0 / .25);
  transition: scale .25s;
}

.ccm-17__planet:hover {
  scale: 1.04;
}

.ccm-17__planet:focus-visible {
  outline: 3px solid var(--star);
  outline-offset: 4px;
}

.ccm-17__sat {
  position: absolute;
  left: 50%;
  top: calc(50% - 92px);
  margin: -23px;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,oklch(0.88 0.14 85),oklch(0.72 0.16 70));
  color: oklch(0.3 0.06 70);
  box-shadow: 0 0 0 4px oklch(0.8 0.16 85 / .2),0 10px 24px -8px oklch(0.7 0.16 75 / .8);
  offset-path: circle(92px at 50% 50%);
  offset-rotate: 0deg;
  offset-distance: var(--park,0%);
  animation: ccm17-orbit 10s linear infinite;
  transition: offset-distance .8s cubic-bezier(.3,1,.35,1),scale .25s;
}

.ccm-17__sat svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
}

.ccm-17__system:hover .ccm-17__sat,
.ccm-17__system:focus-within .ccm-17__sat {
  animation-play-state: paused;
  offset-distance: 0%;
  scale: 1.12;
}

.ccm-17__sat:focus-visible {
  outline: 3px solid var(--star);
  outline-offset: 3px;
}

.ccm-17__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.ccm-17__note {
  position: absolute;
  bottom: 20px;
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: oklch(0.7 0.06 280 / .55);
}

@keyframes ccm17-orbit {
  from {
    offset-distance: 0%;
  }

  to {
    offset-distance: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ccm-17__sat {
    animation: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Circular Menu from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Single Satellite Orbiting Menu Toggle
Source: https://codefronts.com/navigation/css-circular-menus/single-satellite-orbit/

One satellite button endlessly circles a planet-sized CTA, riding a CSS Motion Path — offset-path: circle() — with no keyframe math at all. Hover the system and the satellite eases to a stop at 12 o'clock, ready to click. A striking attention magnet for a single secondary action: 'What's new', a chat bubble, a promo.
## HTML
```html
<section class="ccm-17" aria-label="Orbiting satellite toggle demo">
  <div class="ccm-17__stage">
    <div class="ccm-17__system">
      <span class="ccm-17__track" aria-hidden="true"></span>
      <button type="button" class="ccm-17__planet">Start free trial</button>
      <a class="ccm-17__sat" href="#whats-new"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4l1.8 4.6L18.5 10l-4.7 1.4L12 16l-1.8-4.6L5.5 10l4.7-1.4z"/></svg><span class="ccm-17__sr">What's new</span></a>
    </div>
    <p class="ccm-17__note">offset-path: circle() · hover to dock the satellite</p>
  </div>
</section>
```
## CSS
```css
.ccm-17,
.ccm-17 *,
.ccm-17 *::before,
.ccm-17 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccm-17 {
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 40%,oklch(0.3 0.05 280),oklch(0.19 0.035 290) 75%);
  --brand: oklch(0.62 0.19 265);
  --star: oklch(0.8 0.16 85);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.ccm-17__stage {
  width: 100%;
  height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  border-radius: 20px;
  background: radial-gradient(circle at 50% 40%,oklch(0.3 0.05 280),oklch(0.19 0.035 290) 75%);
  overflow: hidden;
}

.ccm-17__system {
  position: relative;
  width: 240px;
  height: 240px;
  display: grid;
  place-items: center;
}

.ccm-17__track {
  position: absolute;
  inset: 28px;
  border-radius: 50%;
  border: 1px dashed oklch(0.65 0.08 280 / .35);
}

.ccm-17__planet {
  padding: 20px 30px;
  border: none;
  border-radius: 999px;
  background: linear-gradient(140deg,var(--brand),oklch(0.52 0.21 290));
  color: #fff;
  font: 700 16px/1 'Segoe UI',system-ui,sans-serif;
  letter-spacing: .01em;
  cursor: pointer;
  box-shadow: 0 18px 44px -14px oklch(0.5 0.2 275 / .8),inset 0 1px 0 oklch(1 0 0 / .25);
  transition: scale .25s;
}

.ccm-17__planet:hover {
  scale: 1.04;
}

.ccm-17__planet:focus-visible {
  outline: 3px solid var(--star);
  outline-offset: 4px;
}

.ccm-17__sat {
  position: absolute;
  left: 50%;
  top: calc(50% - 92px);
  margin: -23px;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,oklch(0.88 0.14 85),oklch(0.72 0.16 70));
  color: oklch(0.3 0.06 70);
  box-shadow: 0 0 0 4px oklch(0.8 0.16 85 / .2),0 10px 24px -8px oklch(0.7 0.16 75 / .8);
  offset-path: circle(92px at 50% 50%);
  offset-rotate: 0deg;
  offset-distance: var(--park,0%);
  animation: ccm17-orbit 10s linear infinite;
  transition: offset-distance .8s cubic-bezier(.3,1,.35,1),scale .25s;
}

.ccm-17__sat svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
}

.ccm-17__system:hover .ccm-17__sat,
.ccm-17__system:focus-within .ccm-17__sat {
  animation-play-state: paused;
  offset-distance: 0%;
  scale: 1.12;
}

.ccm-17__sat:focus-visible {
  outline: 3px solid var(--star);
  outline-offset: 3px;
}

.ccm-17__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.ccm-17__note {
  position: absolute;
  bottom: 20px;
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: oklch(0.7 0.06 280 / .55);
}

@keyframes ccm17-orbit {
  from {
    offset-distance: 0%;
  }

  to {
    offset-distance: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ccm-17__sat {
    animation: none;
  }
}
```

How this works

The orbit is two lines of Motion Path:

.ccm-17__sat {
  offset-path: circle(92px at 50% 50%);
  animation: ccm17-orbit 10s linear infinite;
}
@keyframes ccm17-orbit { to { offset-distance: 100%; } }

offset-rotate: 0deg keeps the satellite upright instead of banking like a race car — the default rotates the element to follow the path tangent.

The stop-on-hover is the subtle part: animation-play-state: paused freezes it mid-orbit wherever it happens to be; this demo instead transitions offset-distance to 0% (12 o'clock) on :hover/:focus-within, so the target always parks in the same predictable spot before you reach for it.

Make it yours

  • Orbit radius: the circle(92px) — one value.
  • Speed: the 10s duration; add a second satellite with a negative animation-delay to phase-shift it.
  • Elliptical orbit: swap circle() for ellipse().
  • Park position: transition offset-distance to any % — 25% parks at 3 o'clock.

Gotchas — read before shipping

  • Without offset-rotate: 0deg your icon spins upside down every lap.
  • A permanently moving click target is hostile UX — always provide the park-on-hover/focus behavior or a pause control.
  • offset-path animates on the compositor in Chromium but check Safari on heavy pages; keep the satellite's own paint cheap.

Browser support

ChromeSafariFirefoxEdge
116+16+122+116+

offset-path: circle() with offset-distance transitions — baseline since early 2024. Older engines show the satellite parked at 12 o'clock, which is a perfectly usable fallback.

Search CodeFronts

Loading…