UI Components Easy
Orbiting Circles
Multiple circles orbiting around a central element at different speeds and radiuses using pure CSS animation.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
color: #f1f5f9;
}
.orbit-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
.orbit-container {
position: relative;
width: 420px;
height: 420px;
}
/* Central element */
.orbit-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: grid;
place-items: center;
color: white;
box-shadow: 0 0 30px rgba(99, 102, 241, 0.4), 0 0 60px rgba(99, 102, 241, 0.2);
z-index: 10;
}
/* Orbit ring guides */
.orbit-ring {
position: absolute;
top: 50%;
left: 50%;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.06);
pointer-events: none;
}
.orbit-ring--1 {
width: 160px;
height: 160px;
transform: translate(-50%, -50%);
}
.orbit-ring--2 {
width: 260px;
height: 260px;
transform: translate(-50%, -50%);
}
.orbit-ring--3 {
width: 380px;
height: 380px;
transform: translate(-50%, -50%);
}
/* Orbiting circles base */
.orbiting-circle {
position: absolute;
top: 50%;
left: 50%;
width: 36px;
height: 36px;
margin-left: -18px;
margin-top: -18px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
}
.circle-icon {
font-size: 14px;
color: rgba(199, 210, 254, 0.9);
line-height: 1;
}
.circle-icon--lg {
font-size: 16px;
}
/* Orbit 1 — Inner, fast, clockwise */
.orbit--1 {
animation: orbit-spin 8s linear infinite;
}
.orbit--1 .circle-icon {
animation: counter-spin 8s linear infinite;
}
.orbit--1.orbit--delay-0 {
--orbit-radius: 80px;
--start-angle: 0deg;
}
.orbit--1.orbit--delay-1 {
--orbit-radius: 80px;
--start-angle: 180deg;
}
/* Orbit 2 — Middle, medium, counter-clockwise */
.orbit--2 {
animation: orbit-spin 15s linear infinite reverse;
}
.orbit--2 .circle-icon {
animation: counter-spin 15s linear infinite reverse;
}
.orbit--2.orbit--delay-0 {
--orbit-radius: 130px;
--start-angle: 0deg;
}
.orbit--2.orbit--delay-1 {
--orbit-radius: 130px;
--start-angle: 120deg;
}
.orbit--2.orbit--delay-2 {
--orbit-radius: 130px;
--start-angle: 240deg;
}
/* Orbit 3 — Outer, slow, clockwise */
.orbit--3 {
width: 42px;
height: 42px;
margin-left: -21px;
margin-top: -21px;
animation: orbit-spin 22s linear infinite;
}
.orbit--3 .circle-icon {
animation: counter-spin 22s linear infinite;
}
.orbit--3.orbit--delay-0 {
--orbit-radius: 190px;
--start-angle: 0deg;
}
.orbit--3.orbit--delay-1 {
--orbit-radius: 190px;
--start-angle: 90deg;
}
.orbit--3.orbit--delay-2 {
--orbit-radius: 190px;
--start-angle: 180deg;
}
.orbit--3.orbit--delay-3 {
--orbit-radius: 190px;
--start-angle: 270deg;
}
/* Apply radius and starting angle via custom properties */
.orbiting-circle {
transform: rotate(var(--start-angle, 0deg)) translateX(var(--orbit-radius, 100px));
}
@keyframes orbit-spin {
from {
transform: rotate(var(--start-angle, 0deg)) translateX(var(--orbit-radius, 100px));
}
to {
transform: rotate(calc(var(--start-angle, 0deg) + 360deg))
translateX(var(--orbit-radius, 100px));
}
}
@keyframes counter-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(-360deg);
}
}
/* Title */
.orbit-title {
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #e0e7ff 0%, #a78bfa 50%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
}
.orbit-subtitle {
font-size: clamp(0.8rem, 1.8vw, 1rem);
color: rgba(148, 163, 184, 0.7);
text-align: center;
}// Orbiting Circles — pure CSS animation, no JS logic required
// Script kept minimal; animation is entirely CSS-driven.
(function () {
"use strict";
// Optional: pause animation on visibility change to save resources
document.addEventListener("visibilitychange", function () {
const container = document.querySelector(".orbit-container");
if (!container) return;
if (document.hidden) {
container.style.animationPlayState = "paused";
container.querySelectorAll(".orbiting-circle").forEach(function (el) {
el.style.animationPlayState = "paused";
});
} else {
container.style.animationPlayState = "running";
container.querySelectorAll(".orbiting-circle").forEach(function (el) {
el.style.animationPlayState = "running";
});
}
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Orbiting Circles</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="orbit-wrapper">
<div class="orbit-container">
<!-- Central element -->
<div class="orbit-center">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
</div>
<!-- Orbit ring guides -->
<div class="orbit-ring orbit-ring--1"></div>
<div class="orbit-ring orbit-ring--2"></div>
<div class="orbit-ring orbit-ring--3"></div>
<!-- Inner orbit (fast, clockwise) -->
<div class="orbiting-circle orbit--1 orbit--delay-0">
<span class="circle-icon">♦</span>
</div>
<div class="orbiting-circle orbit--1 orbit--delay-1">
<span class="circle-icon">♣</span>
</div>
<!-- Middle orbit (medium, counter-clockwise) -->
<div class="orbiting-circle orbit--2 orbit--delay-0">
<span class="circle-icon">★</span>
</div>
<div class="orbiting-circle orbit--2 orbit--delay-1">
<span class="circle-icon">♥</span>
</div>
<div class="orbiting-circle orbit--2 orbit--delay-2">
<span class="circle-icon">✦</span>
</div>
<!-- Outer orbit (slow, clockwise) -->
<div class="orbiting-circle orbit--3 orbit--delay-0">
<span class="circle-icon circle-icon--lg">☼</span>
</div>
<div class="orbiting-circle orbit--3 orbit--delay-1">
<span class="circle-icon circle-icon--lg">✶</span>
</div>
<div class="orbiting-circle orbit--3 orbit--delay-2">
<span class="circle-icon circle-icon--lg">✿</span>
</div>
<div class="orbiting-circle orbit--3 orbit--delay-3">
<span class="circle-icon circle-icon--lg">☾</span>
</div>
</div>
<h1 class="orbit-title">Orbiting Circles</h1>
<p class="orbit-subtitle">Pure CSS orbital animation with multiple rings</p>
</div>
<script src="script.js"></script>
</body>
</html>import { useMemo } from "react";
interface OrbitItem {
content: React.ReactNode;
}
interface OrbitRingConfig {
items: OrbitItem[];
radius: number;
duration: number;
reverse?: boolean;
}
interface OrbitingCirclesProps {
rings?: OrbitRingConfig[];
centerContent?: React.ReactNode;
className?: string;
}
const defaultCenter = (
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
);
const defaultRings: OrbitRingConfig[] = [
{
items: [{ content: "\u2666" }, { content: "\u2663" }],
radius: 80,
duration: 8,
},
{
items: [{ content: "\u2605" }, { content: "\u2665" }, { content: "\u2726" }],
radius: 130,
duration: 15,
reverse: true,
},
{
items: [
{ content: "\u263C" },
{ content: "\u2736" },
{ content: "\u273F" },
{ content: "\u263E" },
],
radius: 190,
duration: 22,
},
];
export function OrbitingCircles({
rings = defaultRings,
centerContent = defaultCenter,
className = "",
}: OrbitingCirclesProps) {
const keyframesInjected = useMemo(() => {
const styleId = "orbiting-circles-keyframes";
if (typeof document !== "undefined" && !document.getElementById(styleId)) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
@keyframes oc-counter-spin {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
`;
document.head.appendChild(style);
}
return true;
}, []);
const containerSize = Math.max(...rings.map((r) => r.radius)) * 2 + 60;
return (
<div
className={className}
style={{
position: "relative",
width: containerSize,
height: containerSize,
}}
>
{/* Center */}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 56,
height: 56,
borderRadius: "50%",
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
display: "grid",
placeItems: "center",
color: "white",
boxShadow: "0 0 30px rgba(99,102,241,0.4), 0 0 60px rgba(99,102,241,0.2)",
zIndex: 10,
}}
>
{centerContent}
</div>
{/* Ring guides */}
{rings.map((ring, ri) => (
<div
key={`ring-${ri}`}
style={{
position: "absolute",
top: "50%",
left: "50%",
width: ring.radius * 2,
height: ring.radius * 2,
transform: "translate(-50%, -50%)",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.06)",
pointerEvents: "none",
}}
/>
))}
{/* Orbiting items */}
{rings.map((ring, ri) =>
ring.items.map((item, ii) => {
const startAngle = (360 / ring.items.length) * ii;
const dir = ring.reverse ? "reverse" : "normal";
const counterDir = ring.reverse ? "reverse" : "normal";
const size = ring.radius > 150 ? 42 : 36;
const animName = `oc-orbit-${ri}`;
return (
<div
key={`${ri}-${ii}`}
style={{
position: "absolute",
top: "50%",
left: "50%",
width: size,
height: size,
marginLeft: -size / 2,
marginTop: -size / 2,
borderRadius: "50%",
display: "grid",
placeItems: "center",
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
backdropFilter: "blur(8px)",
animation: `${animName} ${ring.duration}s linear infinite ${dir}`,
// @ts-ignore -- inline keyframes via style
["--start-angle" as string]: `${startAngle}deg`,
["--radius" as string]: `${ring.radius}px`,
}}
>
<span
style={{
fontSize: ring.radius > 150 ? 16 : 14,
color: "rgba(199,210,254,0.9)",
lineHeight: 1,
animation: `oc-counter-spin ${ring.duration}s linear infinite ${counterDir}`,
}}
>
{item.content}
</span>
<style>{`
@keyframes ${animName} {
from { transform: rotate(var(--start-angle, 0deg)) translateX(var(--radius, 100px)); }
to { transform: rotate(calc(var(--start-angle, 0deg) + 360deg)) translateX(var(--radius, 100px)); }
}
`}</style>
</div>
);
})
)}
</div>
);
}
// Demo usage
export default function OrbitingCirclesDemo() {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#0a0a0a",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<OrbitingCircles />
<div style={{ textAlign: "center" }}>
<h1
style={{
fontSize: "clamp(1.5rem, 4vw, 2.5rem)",
fontWeight: 800,
letterSpacing: "-0.03em",
background: "linear-gradient(135deg, #e0e7ff 0%, #a78bfa 50%, #8b5cf6 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: "0.5rem",
}}
>
Orbiting Circles
</h1>
<p style={{ fontSize: "1rem", color: "rgba(148,163,184,0.7)" }}>
Pure CSS orbital animation with multiple rings
</p>
</div>
</div>
);
}<script setup lang="ts">
import { computed, onMounted } from "vue";
const props = defineProps({
rings: {
type: Array,
default: () => [
{
items: [{ content: "♦" }, { content: "♣" }],
radius: 80,
duration: 8,
},
{
items: [{ content: "★" }, { content: "♥" }, { content: "✦" }],
radius: 130,
duration: 15,
reverse: true,
},
{
items: [{ content: "☼" }, { content: "✶" }, { content: "✿" }, { content: "☾" }],
radius: 190,
duration: 22,
},
],
},
class: { type: [String, Object, Array], default: "" },
});
const containerSize = computed(
() => Math.max(...(props.rings as any[]).map((r) => r.radius)) * 2 + 60
);
function getItemSize(radius: number) {
return radius > 150 ? 42 : 36;
}
function getFontSize(radius: number) {
return radius > 150 ? 16 : 14;
}
function getOrbitStyle(ring: any, ri: number, ii: number) {
const startAngle = (360 / ring.items.length) * ii;
const dir = ring.reverse ? "reverse" : "normal";
const size = getItemSize(ring.radius);
const animName = `oc-orbit-${ri}`;
return {
position: "absolute",
top: "50%",
left: "50%",
width: size + "px",
height: size + "px",
marginLeft: -size / 2 + "px",
marginTop: -size / 2 + "px",
borderRadius: "50%",
display: "grid",
placeItems: "center",
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
backdropFilter: "blur(8px)",
animation: `${animName} ${ring.duration}s linear infinite ${dir}`,
"--start-angle": `${startAngle}deg`,
"--radius": `${ring.radius}px`,
} as any;
}
function getCounterStyle(ring: any) {
const dir = ring.reverse ? "reverse" : "normal";
return {
fontSize: getFontSize(ring.radius) + "px",
color: "rgba(199,210,254,0.9)",
lineHeight: "1",
animation: `oc-counter-spin ${ring.duration}s linear infinite ${dir}`,
};
}
onMounted(() => {
const styleId = "orbiting-circles-keyframes";
if (!document.getElementById(styleId)) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
@keyframes oc-counter-spin {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
${props.rings
.map(
(ring: any, ri: number) => `
@keyframes oc-orbit-${ri} {
from { transform: rotate(var(--start-angle, 0deg)) translateX(var(--radius, 100px)); }
to { transform: rotate(calc(var(--start-angle, 0deg) + 360deg)) translateX(var(--radius, 100px)); }
}
`
)
.join("")}
`;
document.head.appendChild(style);
}
});
</script>
<template>
<div :class="props.class" :style="{
position: 'relative',
width: containerSize + 'px',
height: containerSize + 'px',
fontFamily: 'system-ui, -apple-system, sans-serif'
}">
<!-- Center -->
<div style="
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 56px; height: 56px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: grid; place-items: center; color: white;
box-shadow: 0 0 30px rgba(99,102,241,0.4), 0 0 60px rgba(99,102,241,0.2);
z-index: 10;
">
<slot name="center">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
</slot>
</div>
<!-- Ring guides -->
<div
v-for="(ring, ri) in rings"
:key="'ring-' + ri"
:style="{
position: 'absolute',
top: '50%',
left: '50%',
width: typeof ring === 'object' && ring !== null && 'radius' in ring ? (ring.radius as number) * 2 + 'px' : '0px',
height: typeof ring === 'object' && ring !== null && 'radius' in ring ? (ring.radius as number) * 2 + 'px' : '0px',
transform: 'translate(-50%, -50%)',
borderRadius: '50%',
border: '1px solid rgba(255,255,255,0.06)',
pointerEvents: 'none',
}"
/>
<!-- Orbiting items -->
<template v-for="(ring, ri) in rings" :key="'orbit-' + ri">
<div
v-if="typeof ring === 'object' && ring !== null && 'items' in ring"
v-for="(item, ii) in ring.items"
:key="ri + '-' + ii"
:style="getOrbitStyle(ring, ri, ii)"
>
<span :style="getCounterStyle(ring)">
{{ typeof item === 'object' && item !== null && 'content' in item ? item.content : '' }}
</span>
</div>
</template>
</div>
</template><script>
import { onMount } from "svelte";
let rings = [
{
items: [{ content: "♦" }, { content: "♣" }],
radius: 80,
duration: 8,
},
{
items: [{ content: "★" }, { content: "♥" }, { content: "✦" }],
radius: 130,
duration: 15,
reverse: true,
},
{
items: [{ content: "☼" }, { content: "✶" }, { content: "✿" }, { content: "☾" }],
radius: 190,
duration: 22,
},
];
$: containerSize = Math.max(...rings.map((r) => r.radius)) * 2 + 60;
onMount(() => {
const styleId = "orbiting-circles-keyframes";
if (!document.getElementById(styleId)) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
@keyframes oc-counter-spin {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
${rings
.map(
(_, ri) => `
@keyframes oc-orbit-${ri} {
from { transform: rotate(var(--start-angle, 0deg)) translateX(var(--radius, 100px)); }
to { transform: rotate(calc(var(--start-angle, 0deg) + 360deg)) translateX(var(--radius, 100px)); }
}
`
)
.join("")}
`;
document.head.appendChild(style);
}
});
function getItemSize(radius) {
return radius > 150 ? 42 : 36;
}
function getFontSize(radius) {
return radius > 150 ? 16 : 14;
}
</script>
<div style="
width: 100vw;
height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
font-family: system-ui, -apple-system, sans-serif;
">
<!-- Orbiting component -->
<div style="position: relative; width: {containerSize}px; height: {containerSize}px;">
<!-- Center -->
<div style="
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 56px; height: 56px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: grid; place-items: center; color: white;
box-shadow: 0 0 30px rgba(99,102,241,0.4), 0 0 60px rgba(99,102,241,0.2);
z-index: 10;
">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
</div>
<!-- Ring guides -->
{#each rings as ring}
<div style="
position: absolute; top: 50%; left: 50%;
width: {ring.radius * 2}px; height: {ring.radius * 2}px;
transform: translate(-50%, -50%); border-radius: 50%;
border: 1px solid rgba(255,255,255,0.06); pointer-events: none;
" />
{/each}
<!-- Orbiting items -->
{#each rings as ring, ri}
{#each ring.items as item, ii}
{@const startAngle = (360 / ring.items.length) * ii}
{@const dir = ring.reverse ? "reverse" : "normal"}
{@const size = getItemSize(ring.radius)}
{@const animName = `oc-orbit-${ri}`}
<div style="
position: absolute; top: 50%; left: 50%;
width: {size}px; height: {size}px;
margin-left: {-size / 2}px; margin-top: {-size / 2}px;
border-radius: 50%; display: grid; place-items: center;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(8px);
animation: {animName} {ring.duration}s linear infinite {dir};
--start-angle: {startAngle}deg;
--radius: {ring.radius}px;
">
<span style="
font-size: {getFontSize(ring.radius)}px;
color: rgba(199,210,254,0.9);
line-height: 1;
animation: oc-counter-spin {ring.duration}s linear infinite {dir};
">
{item.content}
</span>
</div>
{/each}
{/each}
</div>
<!-- Title -->
<div style="text-align: center;">
<h1 style="
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #e0e7ff 0%, #a78bfa 50%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
">Orbiting Circles</h1>
<p style="font-size: 1rem; color: rgba(148,163,184,0.7);">
Pure CSS orbital animation with multiple rings
</p>
</div>
</div>Orbiting Circles
A mesmerizing orbital animation where multiple circles revolve around a central element at varying speeds, radiuses, and directions. Built with pure CSS keyframe animations.
How it works
- A central element is positioned at the midpoint of the container
- Each orbiting circle is absolutely positioned at the center, then offset via
translateX()for radius - A
@keyframesrotation animation spins each circle around the center point - Different
animation-durationvalues and optionalreversedirection create visual depth
Customization
- Adjust the number of orbiting items and their content (icons, text, images)
- Control orbit radius, speed, and direction per circle
- Change colors, sizes, and add trails or glow effects
When to use it
- Hero section decorative backgrounds
- Feature highlights with orbiting icons
- Loading or status indicators
- Interactive data visualizations