A celestial theme dropdown that unfolds into a glowing orbital menu. Choosing a palette lights up the selected row, redraws its checkmark, and lets the whole cluster settle before the loop resets.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Orbital Theme Picker</title>
<link rel="stylesheet" href="orbital-theme-picker.css" />
</head>
<body>
<div class="picker" id="picker">
<button class="trigger" id="trigger" type="button" aria-label="Choose theme" aria-expanded="false">
<span class="trigger-main">
<span class="swatch" aria-hidden="true"></span>
<span class="label-wrap">
<span class="eyebrow">Theme</span>
<span class="label" id="currentLabel">Midnight</span>
</span>
</span>
<span class="chevron" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 9l6 6 6-6"></path>
</svg>
</span>
</button>
<div class="menu" id="menu" aria-hidden="true">
<div class="orbit" aria-hidden="true">
<div class="orbit-ring"></div>
<div class="orbit-dot" style="--angle: 10deg; --delay: 40ms"></div>
<div class="orbit-dot" style="--angle: 88deg; --delay: 120ms"></div>
<div class="orbit-dot" style="--angle: 164deg; --delay: 200ms"></div>
<div class="orbit-dot" style="--angle: 236deg; --delay: 280ms"></div>
<div class="orbit-dot" style="--angle: 314deg; --delay: 360ms"></div>
</div>
<div class="options">
<button class="option selected" type="button" data-value="Midnight">
<span class="option-left">
<span class="theme-dot night" aria-hidden="true"></span>
<span>
<div class="option-name">Midnight</div>
<div class="option-meta">Deep indigo glow</div>
</span>
</span>
<span class="tick" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 10l4 4 8-8"></path>
</svg>
</span>
</button>
<button class="option" type="button" data-value="Sunset">
<span class="option-left">
<span class="theme-dot dusk" aria-hidden="true"></span>
<span>
<div class="option-name">Sunset</div>
<div class="option-meta">Rose and violet haze</div>
</span>
</span>
<span class="tick" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 10l4 4 8-8"></path>
</svg>
</span>
</button>
<button class="option" type="button" data-value="Aurora">
<span class="option-left">
<span class="theme-dot dawn" aria-hidden="true"></span>
<span>
<div class="option-name">Aurora</div>
<div class="option-meta">Soft pink into blue</div>
</span>
</span>
<span class="tick" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 10l4 4 8-8"></path>
</svg>
</span>
</button>
</div>
</div>
</div>
<script src="orbital-theme-picker.js"></script>
</body>
</html>
:root {
--bg: #0b1020;
--panel: rgba(20, 28, 52, 0.82);
--panel-strong: rgba(28, 39, 72, 0.96);
--ring: rgba(129, 140, 248, 0.24);
--line: rgba(148, 163, 184, 0.18);
--text: #eef2ff;
--muted: #a5b4fc;
--shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
--ease: cubic-bezier(0.22, 1, 0.36, 1);
}
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; }
body {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background:
radial-gradient(circle at 50% 50%, rgba(129, 140, 248, 0.16), transparent 28%),
radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.08), transparent 44%),
var(--bg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
color: var(--text);
}
body.no-transition, body.no-transition * {
transition: none !important;
animation: none !important;
}
.picker {
position: relative;
width: 264px;
height: 64px;
}
.trigger {
position: relative;
width: 100%;
height: 64px;
border: 1px solid var(--line);
border-radius: 22px;
background: linear-gradient(180deg, rgba(32, 44, 81, 0.98), rgba(20, 28, 52, 0.98));
box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,0.06);
color: var(--text);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px 0 20px;
cursor: pointer;
overflow: hidden;
transition: border-radius 520ms var(--ease), box-shadow 520ms var(--ease), transform 320ms ease;
}
.picker.open .trigger {
border-radius: 24px 24px 16px 16px;
box-shadow: 0 26px 64px rgba(0,0,0,0.42), 0 0 0 10px rgba(129,140,248,0.08), inset 0 1px 0 rgba(255,255,255,0.08);
}
.trigger::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(120deg, rgba(255,255,255,0.08), transparent 40%, rgba(255,255,255,0.03));
opacity: 0.8;
pointer-events: none;
}
.trigger-main {
display: flex;
align-items: center;
gap: 14px;
position: relative;
z-index: 1;
}
.swatch {
width: 28px;
height: 28px;
border-radius: 50%;
background:
radial-gradient(circle at 30% 30%, rgba(255,255,255,0.95), rgba(255,255,255,0.08) 26%, transparent 28%),
radial-gradient(circle at 68% 70%, rgba(99,102,241,0.85), rgba(29,78,216,0.9) 54%, rgba(15,23,42,1));
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18), 0 0 18px rgba(129,140,248,0.35);
position: relative;
flex: none;
transition: transform 700ms var(--ease), box-shadow 700ms var(--ease);
}
.picker.open .swatch {
transform: rotate(180deg) scale(1.06);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.2), 0 0 24px rgba(129,140,248,0.46);
}
.label-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
line-height: 1.05;
}
.eyebrow {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
opacity: 0.86;
}
.label {
margin-top: 5px;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.01em;
}
.chevron {
position: relative;
width: 24px;
height: 24px;
flex: none;
z-index: 1;
opacity: 0.95;
transition: transform 520ms var(--ease);
}
.picker.open .chevron { transform: rotate(180deg); }
.chevron svg { width: 24px; height: 24px; display: block; }
.menu {
position: absolute;
left: 0;
top: 52px;
width: 100%;
padding: 18px 14px 14px;
border-radius: 0 0 24px 24px;
background: linear-gradient(180deg, rgba(24, 34, 63, 0.82), var(--panel-strong));
border: 1px solid var(--line);
border-top: none;
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(-10px) scale(0.96);
transform-origin: top center;
pointer-events: none;
transition: opacity 320ms ease, transform 520ms var(--ease);
overflow: hidden;
}
.picker.open .menu {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.orbit {
position: absolute;
inset: 8px 16px 10px;
pointer-events: none;
opacity: 0;
transition: opacity 260ms ease;
}
.picker.open .orbit { opacity: 1; }
.orbit-ring {
position: absolute;
left: 50%;
top: 72px;
width: 164px;
height: 164px;
margin-left: -82px;
margin-top: -82px;
border-radius: 50%;
border: 1px solid rgba(129,140,248,0.18);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.02);
}
.orbit-dot {
position: absolute;
left: 50%; top: 72px;
width: 8px; height: 8px;
margin-left: -4px; margin-top: -4px;
border-radius: 50%;
background: rgba(199,210,254,0.95);
box-shadow: 0 0 14px rgba(199,210,254,0.7);
transform: rotate(var(--angle)) translateY(-82px) scale(0.4);
opacity: 0;
}
.picker.open .orbit-dot {
animation: orbit-rise 900ms var(--ease) forwards;
animation-delay: var(--delay);
}
@keyframes orbit-rise {
0% { opacity: 0; transform: rotate(var(--angle)) translateY(-58px) scale(0.2); }
65% { opacity: 0.95; }
100% { opacity: 0.8; transform: rotate(var(--angle)) translateY(-82px) scale(1); }
}
.options {
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 36px;
z-index: 1;
}
.option {
position: relative;
height: 58px;
border: 1px solid rgba(148,163,184,0.14);
border-radius: 18px;
background: rgba(13, 19, 37, 0.5);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px 0 16px;
color: var(--text);
cursor: pointer;
transition: transform 320ms var(--ease), border-color 320ms ease, background 320ms ease, box-shadow 320ms ease;
opacity: 0;
transform: translateY(16px) scale(0.96);
}
.picker.open .option {
opacity: 1;
transform: translateY(0) scale(1);
}
.picker.open .option:nth-child(1) { transition-delay: 80ms; }
.picker.open .option:nth-child(2) { transition-delay: 140ms; }
.picker.open .option:nth-child(3) { transition-delay: 200ms; }
.option.selected {
border-color: rgba(129,140,248,0.42);
background: linear-gradient(180deg, rgba(60,76,146,0.3), rgba(21,30,59,0.76));
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04), 0 0 0 6px rgba(129,140,248,0.08);
}
.option.burst::after {
content: "";
position: absolute;
inset: -8px;
border-radius: 24px;
border: 1px solid rgba(191,219,254,0.65);
opacity: 0;
animation: option-burst 700ms ease forwards;
pointer-events: none;
}
@keyframes option-burst {
0% { opacity: 0.7; transform: scale(0.9); }
100% { opacity: 0; transform: scale(1.1); }
}
.option-left {
display: flex;
align-items: center;
gap: 12px;
}
.theme-dot {
width: 18px;
height: 18px;
border-radius: 50%;
flex: none;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.16);
}
.theme-dot.night {
background: radial-gradient(circle at 32% 32%, #dbeafe, #818cf8 36%, #1e1b4b 70%);
}
.theme-dot.dusk {
background: radial-gradient(circle at 32% 32%, #fde68a, #fb7185 38%, #7c3aed 72%);
}
.theme-dot.dawn {
background: radial-gradient(circle at 32% 32%, #fff7ed, #f9a8d4 34%, #38bdf8 74%);
}
.option-name {
font-size: 17px;
font-weight: 600;
}
.option-meta {
font-size: 13px;
color: rgba(199,210,254,0.8);
margin-top: 3px;
}
.tick {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.12);
display: grid;
place-items: center;
color: #e0e7ff;
background: rgba(255,255,255,0.04);
transition: transform 280ms var(--ease), background 280ms ease, border-color 280ms ease;
}
.option.selected .tick {
background: rgba(129,140,248,0.18);
border-color: rgba(191,219,254,0.34);
transform: scale(1.06);
}
.tick svg {
width: 14px;
height: 14px;
display: block;
}
.tick path {
stroke-dasharray: 20;
stroke-dashoffset: 20;
}
.option.selected .tick path {
animation: draw 340ms 90ms ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
.picker.closing .menu { opacity: 0; transform: translateY(-8px) scale(0.97); }
.picker.closing .option { opacity: 0; transform: translateY(12px) scale(0.96); transition-delay: 0ms !important; }
(function () {
var picker = document.getElementById('picker');
var trigger = document.getElementById('trigger');
var menu = document.getElementById('menu');
var currentLabel = document.getElementById('currentLabel');
var options = Array.prototype.slice.call(document.querySelectorAll('.option'));
var timers = [];
var selected = 'Midnight';
var closing = false;
function clearTimers() {
timers.forEach(function (id) { clearTimeout(id); });
timers = [];
}
function schedule(fn, ms) {
var id = setTimeout(fn, ms);
timers.push(id);
return id;
}
function setOpen(open) {
closing = false;
picker.classList.remove('closing');
picker.classList.toggle('open', open);
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
menu.setAttribute('aria-hidden', open ? 'false' : 'true');
}
function applySelection(value) {
selected = value;
currentLabel.textContent = value;
options.forEach(function (option) {
option.classList.toggle('selected', option.getAttribute('data-value') === value);
});
}
function burst(option) {
option.classList.remove('burst');
void option.offsetWidth;
option.classList.add('burst');
schedule(function () { option.classList.remove('burst'); }, 740);
}
function closeMenuAnimated() {
if (!picker.classList.contains('open')) return;
closing = true;
picker.classList.add('closing');
schedule(function () {
if (!closing) return;
picker.classList.remove('closing');
setOpen(false);
}, 560);
}
trigger.addEventListener('click', function () {
clearTimers();
if (picker.classList.contains('open')) {
closeMenuAnimated();
} else {
setOpen(true);
}
});
options.forEach(function (option) {
option.addEventListener('click', function () {
clearTimers();
applySelection(option.getAttribute('data-value'));
burst(option);
schedule(closeMenuAnimated, 980);
});
});
window.__demo = {
reset: function () {
clearTimers();
closing = false;
document.body.classList.add('no-transition');
picker.classList.remove('open', 'closing');
trigger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-hidden', 'true');
options.forEach(function (option) { option.classList.remove('burst'); });
applySelection('Midnight');
void document.body.offsetHeight;
document.body.classList.remove('no-transition');
}
};
})();