Orbiting Circles
A ring of icons or avatars circling a round orbit, stackable into multiple orbits with reverse directions and different speeds.
A ring of icons or avatars spaced evenly along a circular orbit. Several instances can be stacked into inner and outer orbits, each with its own radius, speed, and direction — a common way to express "a technology ecosystem around a core" or a set of integrations.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add orbiting-circles.enOrbiting Circles is a React orbit animation component. The orbit runs on CSS @keyframes driven by CSS variables, the orbit line is SVG, styling is Tailwind CSS, and Motion is used only for its useReducedMotion hook.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<OrbitingCircles/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- The integrations section of a SaaS homepage: your logo in the middle, the services you connect to circling around it.
- Architecture or ecosystem diagrams, such as a core API surrounded by SDKs, databases, and cloud services.
- Community or team pages, using a ring of avatars to show members.
- Not a good fit: showing dozens of icons at once makes a single orbit crowded; use Icon Cloud instead.
Installation
npx webberui@latest add https://webberui.com/r/orbiting-circles.en.jsonOr, once registries are configured in components.json, install it as @webberui/orbiting-circles.
Usage
children are distributed evenly around the circumference. Stack two instances to build inner and outer orbits, using reverse to send the outer ring the other way and startAngle to stagger the nodes.
import { OrbitingCircles } from "@/components/ui/orbiting-circles";
<div className="relative flex h-[360px] items-center justify-center">
<OrbitingCircles radius={62} duration={16} iconSize={34} className="absolute">
<Github />
<Twitter />
<Figma />
</OrbitingCircles>
<OrbitingCircles
radius={122}
duration={28}
iconSize={42}
reverse
startAngle={20}
className="absolute"
>
<Chrome />
<Slack />
<Cloud />
<Database />
</OrbitingCircles>
</div>Examples
Center logo with one orbit
Center a relative container, put the product logo in the middle, and layer OrbitingCircles on top with absolute.
import { Cloud, Database, Mail, Server } from "lucide-react";
import { OrbitingCircles } from "@/components/ui/orbiting-circles";
export function IntegrationsOrbit() {
return (
<div className="relative flex h-[300px] items-center justify-center">
<div className="flex size-16 items-center justify-center rounded-2xl bg-foreground text-sm font-semibold text-background">
API
</div>
<OrbitingCircles radius={110} duration={24} iconSize={44} className="absolute">
<Database className="size-6" />
<Cloud className="size-6" />
<Server className="size-6" />
<Mail className="size-6" />
</OrbitingCircles>
</div>
);
}Avatar ring without the orbit line
path={false} removes the dashed orbit, and children can be any element, such as round avatar images.
const members = ["/avatars/1.jpg", "/avatars/2.jpg", "/avatars/3.jpg", "/avatars/4.jpg", "/avatars/5.jpg"];
<OrbitingCircles radius={90} iconSize={48} duration={30} path={false}>
{members.map((src) => (
<img key={src} src={src} alt="" className="size-12 rounded-full object-cover ring-2 ring-background" />
))}
</OrbitingCircles>Interleave two sets on the same radius with startAngle
When two instances share a radius and direction, set the second one's startAngle to half the gap between nodes and the two sets alternate.
<div className="relative flex h-[260px] items-center justify-center">
<OrbitingCircles radius={100} duration={20} className="absolute">
<Cloud />
<Server />
<Database />
</OrbitingCircles>
<OrbitingCircles radius={100} duration={20} startAngle={60} path={false} className="absolute">
<Mail />
<Cloud />
<Server />
</OrbitingCircles>
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Elements circling the orbit, distributed evenly around the circumference |
radius | number | 100 | Orbit radius (px) |
duration | number | 20 | Duration of one full lap (seconds); larger is slower |
reverse | boolean | false | Orbit in reverse (counter-clockwise) |
iconSize | number | 40 | Edge length of a single element (px) |
startAngle | number | 0 | Starting angle of the first element (degrees) |
path | boolean | true | Show the faint circular orbit |
className | string | — | Forwarded to the outermost container |
Accessibility
- When the user has "reduce motion" enabled at the system level, the elements land at their static positions on the orbit with no circling animation
- The orbit path is marked
aria-hiddenand is decorative only
FAQ
Does it work with the Next.js App Router?
Yes. The source already has "use client", so import it from a Server Component page directly. The first render matches the server output and only switches to the static version after mount when reduced motion is on, so there is no hydration mismatch.
How do I change the speed and size?
duration is the number of seconds per revolution (higher is slower), radius sets the orbit radius, and iconSize sets the box size of each item. The component's outer size is radius * 2 + iconSize, which is useful when reserving layout space.
Is it expensive to run?
The orbit is a CSS @keyframes transform animation, so there is no per-frame JavaScript and no requestAnimationFrame loop.
How do I change the orbit line color?
The orbit line is an SVG <circle> styled with the stroke-black/10 dark:stroke-white/15 classes in the source; edit them in your copied file, or pass path={false} to hide the line.
What happens when the user prefers reduced motion?
Items sit at fixed positions on the orbit with no circling animation.