Skip to content

Orbiting Circles

Items that travel around a centre on one or more circular paths, such as the tools a product works with. The items are your own children and the orbit is plain CSS.

Motionv0.1.0experimentalWCAG 2.2 AAView spec

Optional, and kept apart.

Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.

import { OrbitingCircles, SparkleIcon, HomeIcon, ChartIcon, BellIcon, SettingsIcon } from "@rdloom/react";

const chip = "flex size-9 items-center justify-center rounded-full border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] text-[var(--rd-color-text-default)]";

export default function OrbitingCirclesBasicExample() {
  return (
    <div className="flex w-full justify-center py-4">
      <OrbitingCircles
        radius={110}
        iconSize={36}
        center={
          <span className="flex size-14 items-center justify-center rounded-full bg-[var(--rd-color-action-primary)] text-[var(--rd-color-action-on-primary)]">
            <SparkleIcon className="size-6" />
          </span>
        }
      >
        <span className={chip}><HomeIcon /></span>
        <span className={chip}><ChartIcon /></span>
        <span className={chip}><BellIcon /></span>
        <span className={chip}><SettingsIcon /></span>
      </OrbitingCircles>
    </div>
  );
}

Installation

npx rdloom add orbiting-circles

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/orbiting-circles.json

Works, but without upgrade tracking, and you import the motion CSS yourself.

Usage

import { OrbitingCircles, SparkleIcon, HomeIcon, ChartIcon, BellIcon, SettingsIcon } from "@rdloom/react";

<div className="flex w-full justify-center py-4">
  <OrbitingCircles
    radius={110}
    iconSize={36}
    center={
      <span className="flex size-14 items-center justify-center rounded-full bg-[var(--rd-color-action-primary)] text-[var(--rd-color-action-on-primary)]">
        <SparkleIcon className="size-6" />
      </span>
    }
  >
    <span className={chip}><HomeIcon /></span>
    <span className={chip}><ChartIcon /></span>
    <span className={chip}><BellIcon /></span>
    <span className={chip}><SettingsIcon /></span>
  </OrbitingCircles>
</div>

Two rings

Core
  • A
  • B
  • C
  • D
  • E
  • F
  • G
import { OrbitingCircles } from "@rdloom/react";

const names = ["A", "B", "C", "D", "E", "F", "G"];

export default function OrbitingCirclesTwoRingsExample() {
  return (
    <div className="flex w-full justify-center py-4">
      <OrbitingCircles
        radius={130}
        rings={2}
        duration={30}
        iconSize={34}
        label="Sample items on two paths"
        center={<span className="text-sm font-medium text-[var(--rd-color-text-default)]">Core</span>}
      >
        {names.map((name) => (
          <span
            key={name}
            className="flex size-[34px] items-center justify-center rounded-full border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-subtle)] text-sm text-[var(--rd-color-text-default)]"
          >
            {name}
          </span>
        ))}
      </OrbitingCircles>
    </div>
  );
}

With pause on hover

import { OrbitingCircles } from "@rdloom/react";

const labels = ["Docs", "Chat", "Plan", "Ship", "Test"];

export default function OrbitingCirclesWithPauseOnHoverExample() {
  return (
    <div className="flex w-full justify-center py-4">
      <OrbitingCircles radius={115} iconSize={52} pauseOnHover reverse showPaths label="Sample links">
        {labels.map((label) => (
          <a
            key={label}
            href="#"
            className="flex size-[52px] items-center justify-center rounded-full border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] text-xs text-[var(--rd-color-text-default)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]"
          >
            {label}
          </a>
        ))}
      </OrbitingCircles>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
childrenrequired

The items that orbit. Each child becomes one list item on a path.

nodenone
center

Content placed in the middle of the paths, such as a logo or a heading.

nodenone
radius

Radius of the outer path in pixels.

number120
rings

How many paths. The children are shared out between them, and neighbouring paths turn in opposite directions.

number1
duration

Seconds for one full turn of the outer path. Inner paths turn faster.

number24
iconSize

Width and height of each item in pixels.

number36
reverse

Turns the outer path the other way.

booleanfalse
showPaths

Draws each path as a thin circle.

booleantrue
pauseOnHover

Pauses while the pointer is over the orbit or focus is inside it.

booleanfalse
label

Accessible name of the list of items.

string"Orbiting items"
isPaused

Stops the orbit. Moving content that lasts more than five seconds should be stoppable.

booleanfalse

Accessibility

Role list, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab: moves through any focusable item, in the order the children were given

Screen readers announce

  • Announced as a list with the given label; each item is read in order as ordinary content

What your code must do

  • The items are real children in a list, in a fixed DOM order, so the order never changes with the motion
  • The drawn paths are decoration: hidden from assistive technology and never take pointer events
  • The orbit is CSS only and starts after the page paints; the server HTML already shows the items spaced evenly
  • It can be stopped: isPaused, an optional pause on hover and focus, and the rdm-still class (WCAG 2.2.2)
  • Visitors who ask for less motion (prefers-reduced-motion) get the items evenly spaced and still
  • Items stay upright while they travel, so text and icons stay readable
  • In forced-colors (high contrast) mode the paths use a system colour
  • Does not flash: nothing changes more than three times in a second (WCAG 2.3.1)

Guidelines

Use it when

  • A hero section that shows the tools or integrations a product connects to
  • A small decorative diagram with a logo in the middle

Avoid it when

  • Essential links that people must be able to hit easily, because moving targets are hard to use
  • More than about eight items on one path

Don't

  • Putting the only way to reach a page on an orbiting item without pause on hover
  • A very short duration, which makes the items hard to follow

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-border-default