Orbiting Icons
Integration icons circling a central logo on two orbits.
components/ui/orbiting-icons.tsx
"use client";
import { motion } from "motion/react";
import { Cloud, Database, Figma, Github, Mail, MessageSquare, Slack, Zap, type LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
type Orbit = { radius: number; duration: number; icons: LucideIcon[]; reverse?: boolean };
const defaultOrbits: Orbit[] = [
{ radius: 80, duration: 18, icons: [Slack, Github, Figma] },
{ radius: 150, duration: 28, icons: [Mail, Database, Cloud, MessageSquare, Zap], reverse: true },
];
const round = (n: number) => Math.round(n * 100) / 100;
export function OrbitingIcons({ orbits = defaultOrbits, center }: { orbits?: Orbit[]; center?: ReactNode }) {
const size = Math.max(...orbits.map((o) => o.radius)) * 2 + 56;
return (
<div className="relative grid place-items-center" style={{ width: size, height: size }}>
{orbits.map((orbit, oi) => {
const spin = { duration: orbit.duration, repeat: Infinity, ease: "linear" as const };
return (
<div
key={oi}
className="absolute rounded-full border border-dashed border-zinc-200"
style={{ width: orbit.radius * 2, height: orbit.radius * 2 }}
>
<motion.div className="absolute inset-0" animate={{ rotate: orbit.reverse ? -360 : 360 }} transition={spin}>
{orbit.icons.map((Icon, i) => {
const angle = (i / orbit.icons.length) * Math.PI * 2;
return (
<motion.div
key={i}
className="absolute grid size-11 place-items-center rounded-xl border border-zinc-200 bg-white shadow-sm"
style={{
left: round(orbit.radius + orbit.radius * Math.cos(angle) - 22),
top: round(orbit.radius + orbit.radius * Math.sin(angle) - 22),
}}
animate={{ rotate: orbit.reverse ? 360 : -360 }}
transition={spin}
>
<Icon className="size-5 text-zinc-700" />
</motion.div>
);
})}
</motion.div>
</div>
);
})}
<div className="relative z-10">{center}</div>
</div>
);
}
export default function Demo() {
return (
<OrbitingIcons
center={
<motion.div
className="grid size-16 place-items-center rounded-2xl bg-zinc-900 text-white shadow-xl"
animate={{ boxShadow: ["0 0 0 0 rgba(37,99,235,0.4)", "0 0 0 16px rgba(37,99,235,0)"] }}
transition={{ duration: 2, repeat: Infinity }}
>
<Zap className="size-7 fill-white" />
</motion.div>
}
/>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/orbiting-icons.tsxcomponents/ui/orbiting-icons.tsx"use client"; import { motion } from "motion/react"; import { Cloud, Database, Figma, Github, Mail, MessageSquare, Slack, Zap, type LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; type Orbit = { radius: number; duration: number; icons: LucideIcon[]; reverse?: boolean }; const defaultOrbits: Orbit[] = [ { radius: 80, duration: 18, icons: [Slack, Github, Figma] }, { radius: 150, duration: 28, icons: [Mail, Database, Cloud, MessageSquare, Zap], reverse: true }, ]; const round = (n: number) => Math.round(n * 100) / 100; export function OrbitingIcons({ orbits = defaultOrbits, center }: { orbits?: Orbit[]; center?: ReactNode }) { const size = Math.max(...orbits.map((o) => o.radius)) * 2 + 56; return ( <div className="relative grid place-items-center" style={{ width: size, height: size }}> {orbits.map((orbit, oi) => { const spin = { duration: orbit.duration, repeat: Infinity, ease: "linear" as const }; return ( <div key={oi} className="absolute rounded-full border border-dashed border-zinc-200" style={{ width: orbit.radius * 2, height: orbit.radius * 2 }} > <motion.div className="absolute inset-0" animate={{ rotate: orbit.reverse ? -360 : 360 }} transition={spin}> {orbit.icons.map((Icon, i) => { const angle = (i / orbit.icons.length) * Math.PI * 2; return ( <motion.div key={i} className="absolute grid size-11 place-items-center rounded-xl border border-zinc-200 bg-white shadow-sm" style={{ left: round(orbit.radius + orbit.radius * Math.cos(angle) - 22), top: round(orbit.radius + orbit.radius * Math.sin(angle) - 22), }} animate={{ rotate: orbit.reverse ? 360 : -360 }} transition={spin} > <Icon className="size-5 text-zinc-700" /> </motion.div> ); })} </motion.div> </div> ); })} <div className="relative z-10">{center}</div> </div> ); } export default function Demo() { return ( <OrbitingIcons center={ <motion.div className="grid size-16 place-items-center rounded-2xl bg-zinc-900 text-white shadow-xl" animate={{ boxShadow: ["0 0 0 0 rgba(37,99,235,0.4)", "0 0 0 16px rgba(37,99,235,0)"] }} transition={{ duration: 2, repeat: Infinity }} > <Zap className="size-7 fill-white" /> </motion.div> } /> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.