Orbiting Icons
component / motion
The integrations diagram: a centre mark with one or more rings of icons travelling around it, evenly distributed and always upright, in pure CSS.
- type
- component
- files
- 2
- registry deps
- none
- npm deps
- none
installs
- components/automindz/orbiting-icons.tsx
- styles/automindz-orbit.css
- Pure CSS: no animation loop, no dependency, renders in a server component
- Icons stay upright — travel and counter-rotation share one duration and one delay
- The field sizes itself to 2 × maxRadius + iconSize, so the outer ring never clips
- Reduced motion stops it fully composed: evenly distributed, upright, still a diagram
- Import styles/automindz-orbit.css once in your layout
The stylesheet is not optional: add import "@/styles/automindz-orbit.css" to your root layout once, or nothing travels. Skipping it is safe, never blank — every icon still sits at its own share of 360°, upright, which is also the prefers-reduced-motion path. OrbitingIcons takes orbits (an array of Orbit elements) and sizes itself to 2 * maxRadius + iconSize; there is no height prop, because a hand-set one is the reason outer rings get shaved. Rings are aria-hidden and inert by default — pass label to a ring whose items carry real accessible names. Radii are px, so render smaller ones below sm if you need two sizes.
Variations
one state per section
- 01Two rings, counter-rotating
- 02Even distribution, and the wrap
- 03Three rings, no centre slot
- 04A labelled ring, with real links
- 05One item, and an empty ring
- 06Standing still is the base state
Two rings, counter-rotating
The default shape. Watch a single glyph for one lap: it travels but never tilts, because the plate's rotation and the icon's are two animations sharing one duration and one delay. The field sized itself to 2 × 132 + 40 = 304px, which is why the outer ring is never shaved as it passes the edge.

Even distribution, and the wrap
Seven on the outer ring: 360/7 is 51.4286°, so the gap between the last glyph and the first across three o'clock is the same as every other gap. The near-miss (dividing by n − 1) puts the seventh on top of the first and leaves a gap you would call a design choice. Three items inside, at 120° apart, going the other way.
Three rings, no centre slot
The centre is optional — this is the 'works with everything' shape, with nothing claiming to be the hub. Three rings at three speeds, and the largest iconSize on any of them is what the field reserves, so the 48px plates on the outer ring clear the edge and the 28px ones simply have room to spare.
A labelled ring, with real links
Rings are aria-hidden and inert by default, because a ring of unlabelled glyphs read out in orbit order is noise. Passing label takes the ring out of that default in one prop: it becomes a named group, its items regain pointer events and the tab order, and each link carries its own name. Tab into the preview to reach them.
One item, and an empty ring
The inner ring is passed no children and renders nothing at all — not an empty circle. The outer one has a single item, which sits at 0° on a ring the field is still sized for, because the icon is at the radius and not at the centre. Neither case needs a guard at the call site.

Standing still is the base state
Turn on Reduce Motion in your OS and reload: the rings stop fully composed — every glyph at its own share of 360°, upright, on its ring. That is also what an install renders with the stylesheet import forgotten, which is why forgetting it is safe. Nothing is hidden, collapsed to the centre, or waiting for a trigger.
