<div class="orbital-wrapper">
<div class="orbit-center">
<img src="https://placehold.co/120x120/1e293b/ffffff?text=User" alt="Avatar" class="center-avatar">
</div>
<div class="orbit-track" id="track-inner">
<!-- Decoupled wrapper for translation, inner card for scaling -->
<div class="orbit-node node-inner"><div class="node-card">HTML</div></div>
<div class="orbit-node node-inner"><div class="node-card">CSS</div></div>
<div class="orbit-node node-inner"><div class="node-card">JS</div></div>
</div>
<div class="orbit-track" id="track-outer">
<div class="orbit-node node-outer"><div class="node-card">React</div></div>
<div class="orbit-node node-outer"><div class="node-card">Node</div></div>
<div class="orbit-node node-outer"><div class="node-card">SQL</div></div>
<div class="orbit-node node-outer"><div class="node-card">Python</div></div>
</div>
</div>
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
margin: 0;
background: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.orbital-wrapper {
position: relative;
width: 600px;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
}
.orbit-center {
position: relative;
z-index: 10;
}
.center-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid #ffffff;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
object-fit: cover;
}
.orbit-track {
position: absolute;
top: 50%;
left: 50%;
border: 1px dashed #cbd5e1;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
#track-inner {
width: 300px;
height: 300px;
}
#track-outer {
width: 480px;
height: 480px;
}
.orbit-node {
position: absolute;
top: 50%;
left: 50%;
/* Shift element center to exact mathematical point */
margin-top: -22px;
margin-left: -40px;
width: 80px;
height: 44px;
pointer-events: auto;
z-index: 5;
}
/*
The hover scale is applied to the child card.
This perfectly prevents the transform conflict bug!
*/
.node-card {
width: 100%;
height: 100%;
background: #ffffff;
border-radius: 22px;
font-weight: 700;
color: #0f172a;
box-shadow: 0 6px 12px rgba(0,0,0,0.08);
border: 2px solid #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s, border-color 0.3s, color 0.3s;
}
.orbit-node:hover {
z-index: 100;
}
.orbit-node:hover .node-card {
transform: scale(1.2);
box-shadow: 0 15px 25px rgba(59, 130, 246, 0.3);
border-color: #3b82f6;
color: #3b82f6;
}
const innerNodes = document.querySelectorAll(".node-inner");
const outerNodes = document.querySelectorAll(".node-outer");
let angleInner = 0;
let angleOuter = 0;
/* Radius must match half the CSS width of the tracks */
const radiusInner = 150;
const radiusOuter = 240;
function animateOrbits() {
/* Slow, elegant rotation speeds */
angleInner += 0.003;
angleOuter -= 0.002;
/* Position Inner Nodes securely */
const innerTotal = innerNodes.length;
for (let i = 0; i < innerTotal; i++) {
const node = innerNodes.item(i);
const slice = (Math.PI * 2) / innerTotal;
const currentAngle = angleInner + (slice * i);
const x = Math.cos(currentAngle) * radiusInner;
const y = Math.sin(currentAngle) * radiusInner;
node.style.transform = "translate(" + x + "px, " + y + "px)";
}
/* Position Outer Nodes securely */
const outerTotal = outerNodes.length;
for (let i = 0; i < outerTotal; i++) {
const node = outerNodes.item(i);
const slice = (Math.PI * 2) / outerTotal;
const currentAngle = angleOuter + (slice * i);
const x = Math.cos(currentAngle) * radiusOuter;
const y = Math.sin(currentAngle) * radiusOuter;
node.style.transform = "translate(" + x + "px, " + y + "px)";
}
window.requestAnimationFrame(animateOrbits);
}
animateOrbits();