Gyroscope
const css = `
@keyframes btn-gyroscope-spin { to { transform: rotate(360deg); } }
.btn-gyroscope {
position: relative;
display: grid;
place-items: center;
width: 180px;
height: 180px;
perspective: 700px;
}
.btn-gyroscope__gimbal {
position: absolute;
inset: 12px;
transform-style: preserve-3d;
transition: transform 0.9s cubic-bezier(0.34, 1.4, 0.64, 1);
pointer-events: none;
}
.btn-gyroscope__gimbal:nth-of-type(1) { transform: rotateX(72deg) rotateY(0deg); }
.btn-gyroscope__gimbal:nth-of-type(2) { transform: rotateX(72deg) rotateY(60deg); inset: 20px; }
.btn-gyroscope__gimbal:nth-of-type(3) { transform: rotateX(72deg) rotateY(-60deg); inset: 28px; }
.btn-gyroscope__ring {
position: absolute;
inset: 0;
border: 1px solid light-dark(#6d7179, #7d8087);
border-radius: 50%;
animation: btn-gyroscope-spin 6s linear infinite;
}
.btn-gyroscope__gimbal:nth-of-type(2) .btn-gyroscope__ring { animation-duration: 4.4s; animation-direction: reverse; }
.btn-gyroscope__gimbal:nth-of-type(3) .btn-gyroscope__ring { animation-duration: 3.2s; }
.btn-gyroscope__ring::after {
content: '';
position: absolute;
top: -3px;
left: calc(50% - 3px);
width: 6px;
height: 6px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 8px light-dark(#373a40, #f1f2f3);
}
.btn-gyroscope__btn {
position: relative;
z-index: 1;
padding: 0.65rem 1.4rem;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 999px;
background: light-dark(#f3f4f7, #1a1c1f);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: box-shadow 0.5s ease, border-color 0.5s ease;
}
.btn-gyroscope__btn:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
/* Gimbal lock: every ring tilts flat into one halo around the button. */
.btn-gyroscope:hover .btn-gyroscope__gimbal,
.btn-gyroscope:focus-within .btn-gyroscope__gimbal { transform: rotateX(0deg) rotateY(0deg); }
.btn-gyroscope:hover .btn-gyroscope__ring,
.btn-gyroscope:focus-within .btn-gyroscope__ring { border-color: light-dark(#50535b, #c6c8cd); }
.btn-gyroscope:hover .btn-gyroscope__btn,
.btn-gyroscope:focus-within .btn-gyroscope__btn {
border-color: light-dark(#50535b, #c6c8cd);
box-shadow: 0 0 24px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 22%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.btn-gyroscope__ring { animation: none; }
.btn-gyroscope__gimbal { transition: none; }
}
`;
export function GyroscopeButton() {
return (
<>
<style>
{css}
</style>
<div className="btn-gyroscope">
<span className="btn-gyroscope__gimbal" aria-hidden="true">
<span className="btn-gyroscope__ring" />
</span>
<span className="btn-gyroscope__gimbal" aria-hidden="true">
<span className="btn-gyroscope__ring" />
</span>
<span className="btn-gyroscope__gimbal" aria-hidden="true">
<span className="btn-gyroscope__ring" />
</span>
<button type="button" className="btn-gyroscope__btn">
Stabilise
</button>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<div class="btn-gyroscope">
<span class="btn-gyroscope__gimbal" aria-hidden="true">
<span class="btn-gyroscope__ring"></span>
</span>
<span class="btn-gyroscope__gimbal" aria-hidden="true">
<span class="btn-gyroscope__ring"></span>
</span>
<span class="btn-gyroscope__gimbal" aria-hidden="true">
<span class="btn-gyroscope__ring"></span>
</span>
<button type="button" class="btn-gyroscope__btn">Stabilise</button>
</div>@keyframes btn-gyroscope-spin { to { transform: rotate(360deg); } }
.btn-gyroscope {
position: relative;
display: grid;
place-items: center;
width: 180px;
height: 180px;
perspective: 700px;
}
.btn-gyroscope__gimbal {
position: absolute;
inset: 12px;
transform-style: preserve-3d;
transition: transform 0.9s cubic-bezier(0.34, 1.4, 0.64, 1);
pointer-events: none;
}
.btn-gyroscope__gimbal:nth-of-type(1) { transform: rotateX(72deg) rotateY(0deg); }
.btn-gyroscope__gimbal:nth-of-type(2) { transform: rotateX(72deg) rotateY(60deg); inset: 20px; }
.btn-gyroscope__gimbal:nth-of-type(3) { transform: rotateX(72deg) rotateY(-60deg); inset: 28px; }
.btn-gyroscope__ring {
position: absolute;
inset: 0;
border: 1px solid light-dark(#6d7179, #7d8087);
border-radius: 50%;
animation: btn-gyroscope-spin 6s linear infinite;
}
.btn-gyroscope__gimbal:nth-of-type(2) .btn-gyroscope__ring { animation-duration: 4.4s; animation-direction: reverse; }
.btn-gyroscope__gimbal:nth-of-type(3) .btn-gyroscope__ring { animation-duration: 3.2s; }
.btn-gyroscope__ring::after {
content: '';
position: absolute;
top: -3px;
left: calc(50% - 3px);
width: 6px;
height: 6px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 8px light-dark(#373a40, #f1f2f3);
}
.btn-gyroscope__btn {
position: relative;
z-index: 1;
padding: 0.65rem 1.4rem;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 999px;
background: light-dark(#f3f4f7, #1a1c1f);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: box-shadow 0.5s ease, border-color 0.5s ease;
}
.btn-gyroscope__btn:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
/* Gimbal lock: every ring tilts flat into one halo around the button. */
.btn-gyroscope:hover .btn-gyroscope__gimbal,
.btn-gyroscope:focus-within .btn-gyroscope__gimbal { transform: rotateX(0deg) rotateY(0deg); }
.btn-gyroscope:hover .btn-gyroscope__ring,
.btn-gyroscope:focus-within .btn-gyroscope__ring { border-color: light-dark(#50535b, #c6c8cd); }
.btn-gyroscope:hover .btn-gyroscope__btn,
.btn-gyroscope:focus-within .btn-gyroscope__btn {
border-color: light-dark(#50535b, #c6c8cd);
box-shadow: 0 0 24px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 22%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.btn-gyroscope__ring { animation: none; }
.btn-gyroscope__gimbal { transition: none; }
}Goal: Create a "Gyroscope" button.
Visual behavior: Three gimbal rings, each with a glowing bead, tumble in 3D around a pill button. Hover or focus locks them: every ring swings flat with a spring into one halo around the button, which lights up.
Technique:
- Three gimbal wrappers with transform-style: preserve-3d sit behind the button at different insets, tilted rotateX(72deg) and turned 0, 60 and -60deg on Y.
- Inside each, a ring (bordered circle with a ::after bead on top) spins on Z at its own speed and direction.
- On hover or focus-within every gimbal transitions to rotateX(0) rotateY(0) on an overshooting cubic-bezier, so the rings land flat together as a halo; the button border and glow brighten.
Accessibility: A real <button> with a visible label; the rings are aria-hidden. Keyboard focus triggers the same lock as hover. Under prefers-reduced-motion the rings stop spinning and the lock is instant.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Gyroscope" button.
Visual behavior: Three gimbal rings, each with a glowing bead, tumble in 3D around a pill button. Hover or focus locks them: every ring swings flat with a spring into one halo around the button, which lights up.
Technique:
- Three gimbal wrappers with transform-style: preserve-3d sit behind the button at different insets, tilted rotateX(72deg) and turned 0, 60 and -60deg on Y.
- Inside each, a ring (bordered circle with a ::after bead on top) spins on Z at its own speed and direction.
- On hover or focus-within every gimbal transitions to rotateX(0) rotateY(0) on an overshooting cubic-bezier, so the rings land flat together as a halo; the button border and glow brighten.
Accessibility: A real <button> with a visible label; the rings are aria-hidden. Keyboard focus triggers the same lock as hover. Under prefers-reduced-motion the rings stop spinning and the lock is instant.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
Three gimbal rings, each with a glowing bead, tumble in 3D around a pill button. Hover or focus locks them: every ring swings flat with a spring into one halo around the button, which lights up.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, 3d, spring, continuous
- Techniques
- preserve 3d, 3d transform, keyframes, css transition
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with a visible label; the rings are aria-hidden. Keyboard focus triggers the same lock as hover. Under prefers-reduced-motion the rings stop spinning and the lock is instant.