← back to Model Arena
data/artifacts/176d578251e3/gpt.html
614 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Radial Spring Menu</title>
<style>
:root {
--bg: #050712;
--btn-bg: #111827;
--btn-bg-active: #1f2937;
--accent: #3b82f6;
--accent-soft: rgba(59, 130, 246, 0.22);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.5);
--size-center: 72px;
--size-item: 46px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at top, #020617 0, #020617 40%, #020617 60%, #000 100%);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
color: #e5e7eb;
overflow: hidden;
}
.radial-menu-wrapper {
position: relative;
width: 260px;
height: 260px;
display: flex;
align-items: center;
justify-content: center;
filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.6));
}
.halo {
position: absolute;
width: 210px;
height: 210px;
border-radius: 50%;
background:
radial-gradient(circle at 30% 15%, rgba(96, 165, 250, 0.09), transparent 55%),
radial-gradient(circle at 75% 85%, rgba(129, 140, 248, 0.09), transparent 55%);
opacity: 0;
transform: scale(0.85);
transition:
opacity 280ms ease,
transform 280ms cubic-bezier(0.18, 0.89, 0.32, 1.28);
pointer-events: none;
}
.radial-menu-wrapper.open .halo {
opacity: 1;
transform: scale(1);
}
.halo::before {
content: "";
position: absolute;
inset: 13%;
border-radius: inherit;
border: 1px dashed rgba(148, 163, 184, 0.35);
border-top-color: rgba(59, 130, 246, 0.7);
border-left-color: rgba(59, 130, 246, 0.35);
animation: orbit 7s linear infinite;
opacity: 0.7;
}
@keyframes orbit {
to {
transform: rotate(360deg);
}
}
.spring-stem {
position: absolute;
width: 2px;
bottom: 50%;
top: 50%;
left: 50%;
pointer-events: none;
transform-origin: 50% 100%;
}
.spring-stem-line {
position: absolute;
width: 100%;
height: 0;
background: linear-gradient(to top, rgba(148, 163, 184, 0.1), rgba(59, 130, 246, 0.7));
border-radius: 999px;
transform-origin: 50% 100%;
opacity: 0;
}
.radial-menu-wrapper.open .spring-stem-line {
opacity: 0.8;
}
.radial-menu {
position: relative;
width: 100%;
height: 100%;
}
.center-button {
position: absolute;
left: 50%;
top: 50%;
width: var(--size-center);
height: var(--size-center);
border-radius: 999px;
background: radial-gradient(circle at 30% 0%, rgba(59, 130, 246, 0.25), transparent 60%) var(--btn-bg);
color: #e5e7eb;
border: 1px solid rgba(148, 163, 184, 0.35);
box-shadow: var(--shadow-soft);
transform: translate(-50%, -50%) scale(1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
position: absolute;
transition:
transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1),
box-shadow 180ms ease,
background 200ms ease,
border-color 200ms ease;
overflow: hidden;
}
.center-button::before {
content: "";
position: absolute;
inset: -40%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.2), transparent 65%);
opacity: 0;
transform: scale(0.6);
transition:
opacity 220ms ease,
transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.center-button:focus-visible {
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9), 0 0 0 3px rgba(59, 130, 246, 0.7);
}
.center-button:hover {
transform: translate(-50%, -50%) scale(1.05);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.8);
border-color: rgba(59, 130, 246, 0.85);
}
.center-button:active {
transform: translate(-50%, -50%) scale(0.96);
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.9);
}
.radial-menu-wrapper.open .center-button {
background: radial-gradient(circle at 30% 0%, rgba(59, 130, 246, 0.3), transparent 60%) var(--btn-bg-active);
}
.radial-menu-wrapper.open .center-button::before {
opacity: 1;
transform: scale(1);
}
.center-icon {
position: relative;
width: 26px;
height: 26px;
}
.center-icon-line {
position: absolute;
left: 4px;
right: 4px;
height: 2px;
border-radius: 999px;
background: linear-gradient(90deg, #60a5fa, #a855f7);
box-shadow: 0 0 6px rgba(37, 99, 235, 0.6);
transform-origin: center;
transition:
transform 280ms cubic-bezier(0.19, 1, 0.22, 1),
opacity 180ms ease;
}
.center-icon-line:nth-child(1) {
top: 8px;
}
.center-icon-line:nth-child(2) {
top: 12px;
}
.center-icon-line:nth-child(3) {
top: 16px;
}
.radial-menu-wrapper.open .center-icon-line:nth-child(1) {
transform: translateY(4px) rotate(45deg);
}
.radial-menu-wrapper.open .center-icon-line:nth-child(2) {
opacity: 0;
transform: scaleX(0.2);
}
.radial-menu-wrapper.open .center-icon-line:nth-child(3) {
transform: translateY(-4px) rotate(-45deg);
}
.items-layer {
position: absolute;
inset: 0;
pointer-events: none;
}
.menu-item {
position: absolute;
width: var(--size-item);
height: var(--size-item);
border-radius: 999px;
background: radial-gradient(circle at 30% 0%, rgba(56, 189, 248, 0.35), transparent 60%) var(--btn-bg);
border: 1px solid rgba(148, 163, 184, 0.45);
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.95);
transform-origin: center;
transform: translate(-50%, -50%) scale(0.2);
opacity: 0;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
color: #e5e7eb;
transition:
transform 210ms ease-out,
opacity 170ms ease-out,
background 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.menu-item::before {
content: "";
position: absolute;
inset: 4px;
border-radius: inherit;
background: radial-gradient(circle at 30% 0%, rgba(59, 130, 246, 0.12), rgba(15, 23, 42, 0.9));
opacity: 0.9;
}
.menu-item-inner {
position: relative;
z-index: 1;
display: flex;
transform: scale(1);
transition: transform 160ms cubic-bezier(0.17, 0.89, 0.32, 1.49);
}
.menu-item svg {
width: 18px;
height: 18px;
fill: none;
stroke: #bfdbfe;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.radial-menu-wrapper.open .menu-item {
pointer-events: auto;
}
.radial-menu-wrapper.open .menu-item[data-state="active"] {
transform: translate(calc(var(--tx, 0px) - 50%), calc(var(--ty, 0px) - 50%)) scale(1);
opacity: 1;
transition-timing-function: cubic-bezier(0.17, 0.89, 0.32, 1.49);
}
.radial-menu-wrapper:not(.open) .menu-item {
transform: translate(-50%, -50%) scale(0.2);
opacity: 0;
}
.menu-item:hover {
background: radial-gradient(circle at 30% 0%, rgba(96, 165, 250, 0.4), transparent 60%) #020617;
border-color: rgba(129, 140, 248, 0.9);
box-shadow: 0 16px 40px rgba(15, 23, 42, 1);
}
.menu-item:hover .menu-item-inner {
transform: scale(1.12) translateY(-1px);
}
.menu-item:active .menu-item-inner {
transform: scale(0.94) translateY(0);
}
.menu-item:active {
box-shadow: 0 7px 16px rgba(15, 23, 42, 1);
}
.menu-item:focus-visible {
outline: none;
box-shadow:
0 0 0 1px rgba(15, 23, 42, 0.9),
0 0 0 3px rgba(96, 165, 250, 0.9),
0 12px 30px rgba(15, 23, 42, 1);
}
.ripple {
position: absolute;
inset: -8px;
border-radius: inherit;
border: 1px solid rgba(59, 130, 246, 0.4);
opacity: 0;
transform: scale(0.6);
pointer-events: none;
}
.ripple.show {
animation: ripple-out 420ms ease-out forwards;
}
@keyframes ripple-out {
0% {
opacity: 0.4;
transform: scale(0.6);
}
75% {
opacity: 0.12;
transform: scale(1.18);
}
100% {
opacity: 0;
transform: scale(1.34);
}
}
.tooltip {
position: fixed;
padding: 4px 8px;
border-radius: 999px;
font-size: 11px;
background: rgba(15, 23, 42, 0.96);
border: 1px solid rgba(148, 163, 184, 0.55);
color: #e5e7eb;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.9);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition:
opacity 120ms ease-out,
transform 120ms ease-out;
z-index: 50;
white-space: nowrap;
}
.tooltip::before {
content: "";
position: absolute;
left: 50%;
bottom: -4px;
transform: translateX(-50%);
border-width: 4px 4px 0 4px;
border-style: solid;
border-color: rgba(148, 163, 184, 0.8) transparent transparent transparent;
}
.tooltip.show {
opacity: 1;
transform: translateY(0);
}
.ring-glow {
position: absolute;
width: 120px;
height: 120px;
border-radius: 999px;
background: radial-gradient(circle, rgba(37, 99, 235, 0.7), transparent 60%);
opacity: 0.35;
filter: blur(20px);
pointer-events: none;
transform: translate(-50%, calc(-50% + 28px)) scale(1);
}
@media (prefers-reduced-motion: reduce) {
* {
scroll-behavior: auto !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
</style>
</head>
<body>
<div class="radial-menu-wrapper" id="radialWrapper" aria-label="Radial quick actions">
<div class="ring-glow"></div>
<div class="halo"></div>
<div class="spring-stem">
<div class="spring-stem-line" id="springStem"></div>
</div>
<div class="radial-menu" aria-hidden="false">
<button class="center-button" id="centerButton" aria-expanded="false" aria-label="Toggle quick actions" type="button">
<div class="center-icon" aria-hidden="true">
<div class="center-icon-line"></div>
<div class="center-icon-line"></div>
<div class="center-icon-line"></div>
</div>
<span class="ripple" aria-hidden="true"></span>
</button>
<div class="items-layer" id="itemsLayer"></div>
</div>
</div>
<div class="tooltip" id="tooltip" role="tooltip"></div>
<script>
(function () {
const wrapper = document.getElementById('radialWrapper');
const centerButton = document.getElementById('centerButton');
const itemsLayer = document.getElementById('itemsLayer');
const tooltip = document.getElementById('tooltip');
const springStem = document.getElementById('springStem');
const buttonsData = [
{ label: 'Home', icon: 'home' },
{ label: 'Search', icon: 'search' },
{ label: 'Notifications', icon: 'bell' },
{ label: 'Profile', icon: 'user' },
{ label: 'Settings', icon: 'settings' },
{ label: 'Favorites', icon: 'star' }
];
function iconSvg(name) {
switch (name) {
case 'home':
return '<svg viewBox="0 0 24 24"><path d="M4 11.5L12 4l8 7.5"></path><path d="M6.5 10.5V19a1 1 0 0 0 1 1H11v-5a1 1 0 0 1 1-1h1.5a1 1 0 0 1 1 1v5H16.5a1 1 0 0 0 1-1v-8.5"></path></svg>';
case 'search':
return '<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="5"></circle><path d="M15 15l3.5 3.5"></path></svg>';
case 'bell':
return '<svg viewBox="0 0 24 24"><path d="M18 15V11a6 6 0 0 0-12 0v4"></path><path d="M5 15h14"></path><path d="M10 18a2 2 0 0 0 4 0"></path></svg>';
case 'user':
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="9" r="3.2"></circle><path d="M6.4 18.5A5.9 5.9 0 0 1 12 15a5.9 5.9 0 0 1 5.6 3.5"></path></svg>';
case 'settings':
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.2"></circle><path d="M19 12.9a7 7 0 0 0 0-1.8l2-1.4-1.8-3.1-2.3.7a6.9 6.9 0 0 0-1.6-.9l-.4-2.4h-3.6l-.4 2.4a6.9 6.9 0 0 0-1.6.9l-2.3-.7L3 9.7l2 1.4a7 7 0 0 0 0 1.8l-2 1.4 1.8 3.1 2.3-.7a6.9 6.9 0 0 0 1.6.9l.4 2.4h3.6l.4-2.4a6.9 6.9 0 0 0 1.6-.9l2.3.7 1.8-3.1z"></path></svg>';
case 'star':
return '<svg viewBox="0 0 24 24"><path d="M12 4.2l1.97 4 4.43.64-3.2 3.1.76 4.42L12 14.9 8.04 16.4l.76-4.42-3.2-3.1 4.43-.64z"></path></svg>';
default:
return '';
}
}
function createButtons() {
const total = buttonsData.length;
const radius = 96;
const startAngle = -120;
const endAngle = -60;
const angleStep = (endAngle - startAngle) / (total - 1);
buttonsData.forEach((data, index) => {
const angleDeg = startAngle + angleStep * index;
const angleRad = angleDeg * (Math.PI / 180);
const tx = Math.cos(angleRad) * radius;
const ty = Math.sin(angleRad) * radius;
const btn = document.createElement('button');
btn.className = 'menu-item';
btn.type = 'button';
btn.setAttribute('aria-label', data.label);
btn.dataset.index = index;
btn.style.setProperty('--tx', tx.toFixed(2) + 'px');
btn.style.setProperty('--ty', ty.toFixed(2) + 'px');
const inner = document.createElement('div');
inner.className = 'menu-item-inner';
inner.innerHTML = iconSvg(data.icon);
btn.appendChild(inner);
const ripple = document.createElement('span');
ripple.className = 'ripple';
btn.appendChild(ripple);
btn.addEventListener('click', () => {
triggerRipple(ripple);
flashStemAlong(index, total);
});
btn.addEventListener('mouseenter', (e) => {
showTooltip(data.label, e.currentTarget);
});
btn.addEventListener('mouseleave', hideTooltip);
btn.addEventListener('focus', (e) => {
showTooltip(data.label, e.currentTarget);
});
btn.addEventListener('blur', hideTooltip);
itemsLayer.appendChild(btn);
});
}
function triggerRipple(el) {
el.classList.remove('show');
// force reflow
void el.offsetWidth;
el.classList.add('show');
}
function showTooltip(text, target) {
tooltip.textContent = text;
const rect = target.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const top = rect.top - tooltipRect.height - 10;
const left = rect.left + rect.width / 2 - tooltipRect.width / 2;
tooltip.style.top = top + 'px';
tooltip.style.left = left + 'px';
tooltip.classList.add('show');
}
function hideTooltip() {
tooltip.classList.remove('show');
}
function flashStemAlong(index, total) {
const baseHeight = 70;
const variation = 18 * Math.abs((index - (total - 1) / 2) / (total / 2));
const height = baseHeight + variation;
springStem.style.height = height + 'px';
springStem.style.marginBottom = '4px';
springStem.style.transition =
'height 260ms cubic-bezier(0.19, 1, 0.22, 1),' +
'opacity 260ms ease';
springStem.style.opacity = '1';
requestAnimationFrame(() => {
setTimeout(() => {
springStem.style.height = '0px';
springStem.style.opacity = '0';
}, 10);
});
}
function setItemsState(open) {
const items = Array.from(itemsLayer.querySelectorAll('.menu-item'));
const stem = springStem;
if (open) {
const maxHeight = 110;
stem.style.height = maxHeight + 'px';
stem.style.marginBottom = '5px';
stem.style.transition =
'height 300ms cubic-bezier(0.19, 1, 0.22, 1),' +
'opacity 300ms ease';
stem.style.opacity = '0.95';
items.forEach((btn, i) => {
const delay = 30 + i * 30;
btn.style.transitionDelay = delay + 'ms';
btn.dataset.state = 'active';
});
setTimeout(() => {
stem.style.height = '80px';
setTimeout(() => {
stem.style.height = '0px';
stem.style.opacity = '0';
}, 160);
}, 260);
} else {
items.forEach((btn, i) => {
const delay = (items.length - i - 1) * 20;
btn.style.transitionDelay = delay + 'ms';
btn.dataset.state = 'inactive';
});
stem.style.height = '0px';
stem.style.opacity = '0';
}
}
function toggleMenu() {
const isOpen = wrapper.classList.toggle('open');
centerButton.setAttribute('aria-expanded', String(isOpen));
setItemsState(isOpen);
const ripple = centerButton.querySelector('.ripple');
triggerRipple(ripple);
}
centerButton.addEventListener('click', toggleMenu);
centerButton.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleMenu();
}
});
createButtons();
})();
</script>
</body>
</html>