Orbiting Circles — <mv-orbit>

Items orbiting a center: multiple rings (radius, duration, direction), even spacing, icons that stay upright, drawn paths; 100% CSS animation, paused when off-screen.

CategoryLayout
TypeWeb Component (<mv-orbit>)
Statusstable
Keywordsorbit, circles, integrations, rotation, hero, decorative

When to use

Avoid when

Install

node scripts/add.mjs orbiting-circles --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["orbiting-circles"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/observe.js, components/orbiting-circles/orbiting-circles.js, components/orbiting-circles/orbiting-circles.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="orbit-demo" style="display:grid;place-items:center;width:100%">
  <style>
    #orbit-demo .ob-core { display: grid; place-items: center; width: 4.5rem; height: 4.5rem; border-radius: 1.4rem; color: #fff;
      background: linear-gradient(145deg, oklch(0.72 0.17 var(--mv-accent-h)), oklch(0.48 0.24 calc(var(--mv-accent-h) + 20)));
      box-shadow: 0 0 0 8px color-mix(in oklch, var(--mv-accent) 12%, transparent), 0 20px 50px -10px color-mix(in oklch, var(--mv-accent) 60%, transparent); }
    #orbit-demo .ob-core svg { width: 2.2rem; height: 2.2rem; }
    #orbit-demo .ob-node { border-radius: 50%;
      border: 1px solid var(--mv-border); background: var(--mv-surface-raised); color: var(--mv-fg); box-shadow: var(--mv-shadow-md); }
    #orbit-demo .ob-node svg { width: 48%; height: 48%; }
    #orbit-demo .ob-node[style*="--hue"] { border: 0; color: #fff; background: linear-gradient(145deg, oklch(0.74 0.15 var(--hue)), oklch(0.52 0.2 calc(var(--hue) + 30))); }
    #orbit-demo .ob-av { border-radius: 50%; font-size: .75rem; font-weight: 700; color: #fff;
      border: 2px solid var(--mv-bg); box-shadow: var(--mv-shadow-md); background: linear-gradient(135deg, oklch(0.75 0.14 var(--hue)), oklch(0.5 0.19 calc(var(--hue) + 40))); }
  </style>

  <mv-orbit path aria-label="Integrations ecosystem" role="img">
    <div data-center><span class="ob-core"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18V7l7 6 7-6v11"/></svg></span></div>

    <div class="mv-orbit-ring" data-radius="78" data-duration="14" style="--mv-orbit-item-size: 2.5rem">
      <span class="ob-node" style="--hue:250"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></span>
      <span class="ob-node" style="--hue:150"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z"/></svg></span>
      <span class="ob-node" style="--hue:25"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></span>
    </div>

    <div class="mv-orbit-ring" data-radius="140" data-duration="26" data-reverse data-path="dashed">
      <span class="ob-node"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></span>
      <span class="ob-node"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/></svg></span>
      <span class="ob-node"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.3A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg></span>
      <span class="ob-node"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m7 15 4-4 3 3 5-6"/></svg></span>
      <span class="ob-node"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>
    </div>

    <div class="mv-orbit-ring" data-radius="200" data-duration="44" style="--mv-orbit-item-size: 2.4rem; --mv-orbit-offset: 20deg">
      <span class="ob-av" style="--hue:280">PS</span>
      <span class="ob-av" style="--hue:200">KT</span>
      <span class="ob-av" style="--hue:330">AO</span>
      <span class="ob-av" style="--hue:30">LB</span>
      <span class="ob-av" style="--hue:150">MR</span>
      <span class="ob-av" style="--hue:90">TN</span>
    </div>
  </mv-orbit>
</div>

API

Attributes

NameTypeDefaultDescription
radiusnumber120Radius (px) of the default ring (children outside any .mv-orbit-ring).
durationnumber20Duration of one revolution (s) of the default ring.
reversebooleanReverse direction for the default ring.
pathbooleanDraws each orbit’s circle.
pausedbooleanFreezes the orbits.

Content structure

NameDescription
[data-center]Centered element (logo, avatar…).
.mv-orbit-ringA ring: data-radius (px), data-duration (s), data-reverse, data-path="dashed|none". Its children orbit.
other childrenGrouped into a default ring driven by the element’s attributes.

CSS variables

NameDefaultDescription
--mv-orbit-size2 × max radius + 64pxSize of the square container.
--mv-orbit-item-size2.75remItem size (can be set per ring).
--mv-orbit-offset0degStarting angular offset of a ring.
--mv-orbit-radiusRadius in any CSS unit (takes precedence over data-radius).
--mv-orbit-path-color / --mv-orbit-path-styleColor and style of the orbit stroke.

Accessibility

Purely visual: give the element role="img" + aria-label (or aria-hidden) when the satellites are decorative. Reduced motion: the constellation stays frozen at its starting angles. Pauses automatically when off-screen.