Browse products

Orbit Loop

Twelve integration nodes circle an accent hub on three rings at 1, 2/3 and 2/5 speed. Hover or focus one and the whole system eases to a stop, a dashed line links it to the core and its details slide in.

Loading

AI prompt

Recreate this effect with any AI tool

Paste into Claude, Cursor, v0, Lovable or Bolt. 1322 words of exact values — rewritten live as you tune the Playground.

prompt.md · Orbit Loop
### EFFECT: Orbit Loop
**What it does (visual):** A two-column dark page. Left: mono eyebrow, a large serif headline "One core, *twelve* integrations." (*twelve* italic in `#d4ff3a`), a muted paragraph and a detail card (ring number in accent, category, service name in serif, one-line description). Right: a square stage with three concentric rings (inner and outer hairlines, the middle one dotted) around a 124px `#d4ff3a` hub disc that reads "Atlas / CORE" with two soft accent halos. Twelve 52px circular chips — 3 on the inner ring, 4 on the middle, 5 on the outer, each a line icon with a mono uppercase label underneath — circle the hub continuously and linearly: the inner ring makes one revolution every 24s, the middle ring every 1.5 × 24s and the outer every 2.5 × 24s (ring direction `same`: `alternate` sends the middle ring the other way). Nodes on each ring are evenly spaced and every ring is offset by 30° from the previous one. Ring radii are 20%, 32.5% and 45% of the stage size × 1. With tilt 0° the rings flatten into ellipses (`ry = rx × cos(tilt)`) and nodes at the front grow up to 16%, become fully opaque and pass in front of the hub while the back ones shrink, fade to 62% and slip behind it. Orientation `upright`: `upright` keeps every label level, `tangent` turns each chip to face its direction of travel. Hovering, focusing or clicking a chip eases the whole system to a stop in 0.6s, fills that chip with the accent (scale 1.14), draws a dashed accent line from the hub to it and slides its details into the card; leaving eases the orbit back up over 0.8s. A pill dock has Pause/Play and the readout "24 s / inner orbit". On load, a slow tour (orbit at 0.35×) highlights a node every 2.6s until the first pointerdown or key press.

**Tech stack:**
- GSAP 3.12+ core (`https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js` or `npm i gsap` → `import gsap from "gsap"`).
- An absolutely positioned SVG for the ring ellipses and the connector line; nodes are HTML `<button>`s inside a `<ul>` (keyboard-accessible) positioned with `transform`.
- Fonts: serif display (demo: Fraunces), mono labels (JetBrains Mono), sans body (Inter Tight).

**HTML structure:**
```html
<div class="orb">
  <section class="orb__copy">
    <h1 class="orb__h">One core, <em>twelve</em> integrations.</h1>
    <div class="orb__detail" aria-live="polite">
      <p class="orb__detail-k"><span class="orb__detail-ring">Ring 01</span> · <span class="orb__detail-cat">Identity</span></p>
      <p class="orb__detail-name">Atlas core</p><p class="orb__detail-desc">…</p>
    </div>
  </section>
  <section class="orb__stage" aria-label="Integration ecosystem">
    <svg class="orb__rings" aria-hidden="true">
      <ellipse class="orb__ring" data-ring="0"/><ellipse class="orb__ring" data-ring="1"/><ellipse class="orb__ring" data-ring="2"/>
      <line class="orb__link"/>
    </svg>
    <div class="orb__hub" aria-hidden="true"><span class="orb__hub-name">Atlas</span><span class="orb__hub-sub">core</span></div>
    <ul class="orb__nodes" role="list">
      <!-- ×12, generated from data: { ring, name, cat, icon, desc } -->
      <li class="orb__item"><button class="orb__node" type="button" aria-label="Auth — Identity">
        <span class="orb__chip"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 11V8a5 5 0 0 1 10 0v3M5 11h14v9H5z"/></svg></span>
        <span class="orb__label" aria-hidden="true">Auth</span></button></li>
    </ul>
  </section>
  <div class="orb__dock" role="group" aria-label="Orbit controls">
    <button class="orb__btn orb__pause" type="button" aria-pressed="false">Pause</button>
    <span class="orb__readout"><b>24</b> s / inner orbit</span>
  </div>
</div>
```
Nodes: inner — Auth, Payments, Search; middle — Mail, Storage, Analytics, Maps; outer — CMS, Chat, Calendar, Video, Forms.

**CSS requirements:**
- `.orb`: `display: grid; grid-template-columns: minmax(0,.8fr) minmax(0,1.2fr); align-items: center; min-height: 100vh; overflow: hidden; background: var(--bg)`. Under 900px: one column, `--orb-node: 40px`, labels hidden except on the active/focused node.
- `.orb__stage`: `position: relative; width: 100%; max-width: 680px; aspect-ratio: 1`. `.orb__rings`: `position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; overflow: visible; pointer-events: none`.
- Rings: `fill: none; stroke: color-mix(in srgb, var(--text) 13%, transparent)`; middle ring `stroke-dasharray: 2 6` at 22%. Link: `stroke: var(--accent); stroke-dasharray: 3 4; opacity: 0 → .9` (transition .35s) when active.
- `.orb__hub`: centred with `translate(-50%,-50%)`, `width: clamp(64px, 17%, 124px); aspect-ratio: 1; border-radius: 50%; background: var(--accent); z-index: 50`, halos `box-shadow: 0 0 0 10px accent/10%, 0 0 0 22px accent/5%`.
- `.orb__item`: `position: absolute; left: 0; top: 0; width/height: var(--orb-node); margin: calc(var(--orb-node) / -2) 0 0 calc(var(--orb-node) / -2); will-change: transform` (so `translate(x, y)` targets the chip centre).
- `.orb__chip`: 1px border at 16% text, `var(--surface)` fill, icon at 44% size, stroke 1.6. Active / hover / focus-visible: `background: var(--accent); color: #0A0A0A; scale: 1.14` (transition .5s `cubic-bezier(.16,1,.3,1)`), focus ring `0 0 0 3px var(--bg), 0 0 0 5px var(--accent)`.

**Animation spec (exact values):**
- `RINGS = [{ r: .2, speed: 1 }, { r: .325, speed: 2/3 }, { r: .45, speed: 2/5 }]`; `CYCLE = 15` (15, 10 and 6 whole turns).
- Clock: `gsap.fromTo(clock, { t: 0 }, { t: 15, duration: 24 × 15, ease: 'none', repeat: -1, onUpdate: render })`.
- `render()`: for node k of n on ring r: `deg = −90 + 360·k/n + r·30 + dir · speed_r · t · 360` (dir = −1 for ring 1 when `same` is `alternate`), `a = deg·π/180`, `R = r_r · stageSize · 1`, `depth = sin a`, `x = cx + cos a · R`, `y = cy + depth · R · cos(0°)`, `scale = 1 + 0.16 · depth · sin(0°)`. Write `li.style.transform = translate3d(x, y, 0) scale(scale)`; when tilt > 0 also `opacity = 0.62 + 0.38 · (depth/2 + 0.5)` and `z-index = round(40 + depth·30)` (hub is 50). `upright` `tangent`: `button.style.transform = rotate(deg + 90°·dir)`; `upright`: no rotation. Update the ellipses: `rx = R`, `ry = R·cos(tilt)`, and the link `x2/y2` to the active node.
- Speed: `speed = { play, hold, tour }`, `clockTween.timeScale(play × hold × tour)` in the `onUpdate` of `gsap.to(speed, { …, ease: 'power2.inOut', overwrite: 'auto' })`. Hover/focus → `hold: 0` over 0.6s; leave/blur → `hold: 1` over 0.8s; Pause → `play: 0 | 1` over 0.7s; tour → `tour: 0.35` over 0.8s, back to 1 on first input.
- Detail swap: `gsap.fromTo([name, desc], { y: 10, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, ease: 'power3.out', stagger: 0.05 })`.
- Tour: `gsap.delayedCall(1.6, step)`, each step highlights the next of nodes [1, 5, 9, 3, 7, 11] and re-schedules itself after 2.6s.

**Interaction:** `pointerenter`/`pointerleave` on each `<li>` (static wrapper) plus `focus`/`blur`/`click` on its button. A capture-phase `pointerdown`/`keydown` on the root ends the tour. Pause toggles `aria-pressed` and its label (Pause ↔ Play). Off-screen (`ml:pause` / IntersectionObserver) the clock tween is paused and resumed.

**Responsive behavior:** A `ResizeObserver` on the stage re-measures `stageSize` and re-renders, so ring radii always follow the layout. Under 900px the grid stacks (copy, detail card, stage), chips shrink to 40px and labels show only for the active/focused node so they never collide. Periods do not change with screen size.

**Accessibility:** Every node is a `<button>` with `aria-label="Name — Category"`; tabbing through them stops the orbit and announces the detail card (`aria-live="polite"`). Focus rings are visible. The Pause button (`aria-pressed`) satisfies WCAG 2.2.2 for continuous motion. Under `prefers-reduced-motion: reduce` there is no clock tween and no tour: nodes are rendered once at t = 0, hover/focus still updates the card and connector instantly, the Pause button is disabled and reads "Static".

**Performance rules:** One tween for the whole system; `render()` only writes `transform`, `opacity` and `z-index` on 12 elements plus a few SVG attributes. `will-change: transform` on the items only. Wrap in `gsap.context()`; on teardown `ctx.revert()`, abort listeners (AbortController), disconnect the ResizeObserver and kill the tour call. Pause while off-screen.

**Customization knobs:** inner orbit 24s (rings at 1, 2/3, 2/5 speed), radius 1×, ring phase 30°, tilt 0°, orientation `upright`, ring direction `same`, accent `#d4ff3a`. Node data (name, category, icon path, description) lives in one array.

**Acceptance criteria:**
- [ ] The inner ring completes a revolution in exactly 24s; the loop never jumps when the clock repeats.
- [ ] Labels stay level in `upright` mode; chips face their direction of travel in `tangent` mode.
- [ ] Hover or focus eases every node to a stop (no snap) and shows the connector + details; leaving resumes smoothly.
- [ ] With tilt, front nodes pass over the hub and back nodes behind it.
- [ ] Pause button works; reduced motion shows a static diagram; no console errors or horizontal overflow.

Source

Full source code

<div class="orb">
  <section class="orb__copy">
    <p class="orb__eyebrow">SVG · Loop · 083</p>
    <h1 class="orb__h">One core, <em>twelve</em> integrations.</h1>
    <p class="orb__p">Every service circles the platform on its own ring. Hover or focus a node and the system eases to a stop so you can read it.</p>

    <div class="orb__detail" aria-live="polite">
      <p class="orb__detail-k"><span class="orb__detail-ring">Ring 01</span><span class="orb__detail-sep">·</span><span class="orb__detail-cat">Identity</span></p>
      <p class="orb__detail-name">Atlas core</p>
      <p class="orb__detail-desc">Twelve services, three orbits. Inner rings turn fastest, the outer ring takes the long way round.</p>
    </div>
  </section>

  <section class="orb__stage" aria-label="Integration ecosystem">
    <svg class="orb__rings" aria-hidden="true">
      <ellipse class="orb__ring" data-ring="0"/>
      <ellipse class="orb__ring" data-ring="1"/>
      <ellipse class="orb__ring" data-ring="2"/>
      <line class="orb__link"/>
    </svg>
    <div class="orb__hub" aria-hidden="true">
      <span class="orb__hub-name">Atlas</span>
      <span class="orb__hub-sub">core</span>
    </div>
    <ul class="orb__nodes" role="list"></ul>
  </section>

  <div class="orb__dock" role="group" aria-label="Orbit controls">
    <button class="orb__btn orb__pause" type="button" aria-pressed="false">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path class="orb__ico-pause" d="M9 6v12M15 6v12"/><path class="orb__ico-play" d="M8 5.5v13l11-6.5z"/></svg>
      <span class="orb__btn-label">Pause</span>
    </button>
    <span class="orb__readout"><b class="orb__readout-n">24</b> s / inner orbit</span>
  </div>
</div>