About this snippet

Showcase your tech stack using an interactive orbital skill tree. We decoupled the hover scaling from the orbit translation, meaning the nodes orbit smoothly and scale flawlessly when hovered without any buggy jumping.

Size
4.1 KB
Uses
HTML, CSS, JavaScript
Loads
placehold.co
Views
218
<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();
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save