26 CSS Circular Menus02 / 26

Pure CSSMIT licensed

Pure CSS Radial Submenu (sin() & cos())

Zero JavaScript. A hidden checkbox drives the open state, and CSS trigonometric functions — sin() and cos(), shipped in every 2023+ browser — place six links on a perfect ring. This is the reference implementation to learn radial math from: one angle formula, one radius token, and the whole menu is generative.

Published

Live Demo
Try it

The code

<section class="ccm-02" aria-label="Pure CSS radial submenu demo">
  <div class="ccm-02__stage">
    <div class="ccm-02__hub" style="--n:6">
      <input type="checkbox" id="ccm02-t" class="ccm-02__state">
      <ul class="ccm-02__orbit">
        <li class="ccm-02__item" style="--i:0"><a href="#home" aria-label="Home"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11l8-7 8 7v9h-5v-6h-6v6H4z"/></svg></a></li>
        <li class="ccm-02__item" style="--i:1"><a href="#search" aria-label="Search"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.5-4.5"/></svg></a></li>
        <li class="ccm-02__item" style="--i:2"><a href="#stats" aria-label="Analytics"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 20V10M12 20V4M19 20v-7"/></svg></a></li>
        <li class="ccm-02__item" style="--i:3"><a href="#chat" aria-label="Messages"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h16v11H9l-5 4z"/></svg></a></li>
        <li class="ccm-02__item" style="--i:4"><a href="#saved" aria-label="Saved"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 4h12v17l-6-4-6 4z"/></svg></a></li>
        <li class="ccm-02__item" style="--i:5"><a href="#settings" aria-label="Settings"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1"/></svg></a></li>
      </ul>
      <label for="ccm02-t" class="ccm-02__toggle"><span class="ccm-02__bar"></span><span class="ccm-02__sr">Toggle menu</span></label>
    </div>
    <p class="ccm-02__note">No JavaScript on this one — inspect it.</p>
  </div>
</section>
.ccm-02,
.ccm-02 *,
.ccm-02 *::before,
.ccm-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccm-02 {
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 40%,oklch(0.35 0.06 175),oklch(0.22 0.04 190));
  --brand: oklch(0.7 0.16 160);
  --deep: oklch(0.28 0.05 170);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.ccm-02__stage {
  width: 100%;
  height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  border-radius: 20px;
  background: radial-gradient(circle at 50% 40%,oklch(0.35 0.06 175),oklch(0.22 0.04 190));
  overflow: hidden;
}

.ccm-02__hub {
  position: relative;
  width: 64px;
  height: 64px;
}

.ccm-02__state {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.ccm-02__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.ccm-02__toggle {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand);
  color: var(--deep);
  cursor: pointer;
  box-shadow: 0 10px 30px -8px oklch(0.7 0.16 160 / .6);
  transition: rotate .4s cubic-bezier(.34,1.3,.4,1);
}

.ccm-02__bar {
  position: relative;
  width: 22px;
  height: 2.5px;
  border-radius: 2px;
  background: currentColor;
  transition: rotate .35s;
}

.ccm-02__bar::before,
.ccm-02__bar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: currentColor;
  transition: translate .35s,rotate .35s;
}

.ccm-02__bar::before {
  translate: 0 -7px;
}

.ccm-02__bar::after {
  translate: 0 7px;
}

.ccm-02__hub:has(:checked) .ccm-02__toggle {
  rotate: 90deg;
}

.ccm-02__hub:has(:checked) .ccm-02__bar {
  rotate: 45deg;
}

.ccm-02__hub:has(:checked) .ccm-02__bar::before {
  translate: 0 0;
  rotate: -90deg;
}

.ccm-02__hub:has(:checked) .ccm-02__bar::after {
  translate: 0 0;
  opacity: 0;
}

.ccm-02__hub:has(.ccm-02__state:focus-visible) .ccm-02__toggle {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

.ccm-02__orbit {
  position: absolute;
  inset: 0;
  list-style: none;
  --r: 0px;
  transition: --r .5s;
}

.ccm-02__item {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  --a: calc((360deg / var(--n)) * var(--i) - 90deg);
  translate: calc(cos(var(--a)) * var(--r)) calc(sin(var(--a)) * var(--r));
  opacity: 0;
  scale: .4;
  visibility: hidden;
  transition: translate .5s cubic-bezier(.34,1.35,.45,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.45,1),visibility 0s .5s;
  transition-delay: calc(var(--i) * 35ms);
}

.ccm-02__hub:has(:checked) .ccm-02__item {
  --r: 120px;
  opacity: 1;
  scale: 1;
  visibility: visible;
  transition: translate .5s cubic-bezier(.34,1.35,.45,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.45,1),visibility 0s;
  transition-delay: calc(var(--i) * 45ms);
}

.ccm-02__item a {
  pointer-events: auto;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: oklch(0.98 0.01 160 / .1);
  border: 1px solid oklch(0.9 0.05 160 / .25);
  backdrop-filter: blur(6px);
  color: oklch(0.92 0.04 160);
  transition: background .25s,color .25s,scale .25s;
}

.ccm-02__item a:hover,
.ccm-02__item a:focus-visible {
  background: var(--brand);
  color: var(--deep);
  scale: 1.12;
  outline: none;
}

.ccm-02__item svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ccm-02__note {
  position: absolute;
  bottom: 20px;
  left: 0;
  right: 0;
  text-align: center;
  font-size: 12.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: oklch(0.75 0.04 165 / .6);
}

@media (prefers-reduced-motion: reduce) {
  .ccm-02 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Circular Menu from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Pure CSS Radial Submenu (sin() & cos())
Source: https://codefronts.com/navigation/css-circular-menus/pure-css-radial-submenu-sin-and-cos/

Zero JavaScript. A hidden checkbox drives the open state, and CSS trigonometric functions — sin() and cos(), shipped in every 2023+ browser — place six links on a perfect ring. This is the reference implementation to learn radial math from: one angle formula, one radius token, and the whole menu is generative.
## HTML
```html
<section class="ccm-02" aria-label="Pure CSS radial submenu demo">
  <div class="ccm-02__stage">
    <div class="ccm-02__hub" style="--n:6">
      <input type="checkbox" id="ccm02-t" class="ccm-02__state">
      <ul class="ccm-02__orbit">
        <li class="ccm-02__item" style="--i:0"><a href="#home" aria-label="Home"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11l8-7 8 7v9h-5v-6h-6v6H4z"/></svg></a></li>
        <li class="ccm-02__item" style="--i:1"><a href="#search" aria-label="Search"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.5-4.5"/></svg></a></li>
        <li class="ccm-02__item" style="--i:2"><a href="#stats" aria-label="Analytics"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 20V10M12 20V4M19 20v-7"/></svg></a></li>
        <li class="ccm-02__item" style="--i:3"><a href="#chat" aria-label="Messages"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h16v11H9l-5 4z"/></svg></a></li>
        <li class="ccm-02__item" style="--i:4"><a href="#saved" aria-label="Saved"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 4h12v17l-6-4-6 4z"/></svg></a></li>
        <li class="ccm-02__item" style="--i:5"><a href="#settings" aria-label="Settings"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1"/></svg></a></li>
      </ul>
      <label for="ccm02-t" class="ccm-02__toggle"><span class="ccm-02__bar"></span><span class="ccm-02__sr">Toggle menu</span></label>
    </div>
    <p class="ccm-02__note">No JavaScript on this one — inspect it.</p>
  </div>
</section>
```
## CSS
```css
.ccm-02,
.ccm-02 *,
.ccm-02 *::before,
.ccm-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccm-02 {
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 40%,oklch(0.35 0.06 175),oklch(0.22 0.04 190));
  --brand: oklch(0.7 0.16 160);
  --deep: oklch(0.28 0.05 170);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.ccm-02__stage {
  width: 100%;
  height: 100vh;
  position: relative;
  display: grid;
  place-items: center;
  border-radius: 20px;
  background: radial-gradient(circle at 50% 40%,oklch(0.35 0.06 175),oklch(0.22 0.04 190));
  overflow: hidden;
}

.ccm-02__hub {
  position: relative;
  width: 64px;
  height: 64px;
}

.ccm-02__state {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.ccm-02__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.ccm-02__toggle {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand);
  color: var(--deep);
  cursor: pointer;
  box-shadow: 0 10px 30px -8px oklch(0.7 0.16 160 / .6);
  transition: rotate .4s cubic-bezier(.34,1.3,.4,1);
}

.ccm-02__bar {
  position: relative;
  width: 22px;
  height: 2.5px;
  border-radius: 2px;
  background: currentColor;
  transition: rotate .35s;
}

.ccm-02__bar::before,
.ccm-02__bar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: currentColor;
  transition: translate .35s,rotate .35s;
}

.ccm-02__bar::before {
  translate: 0 -7px;
}

.ccm-02__bar::after {
  translate: 0 7px;
}

.ccm-02__hub:has(:checked) .ccm-02__toggle {
  rotate: 90deg;
}

.ccm-02__hub:has(:checked) .ccm-02__bar {
  rotate: 45deg;
}

.ccm-02__hub:has(:checked) .ccm-02__bar::before {
  translate: 0 0;
  rotate: -90deg;
}

.ccm-02__hub:has(:checked) .ccm-02__bar::after {
  translate: 0 0;
  opacity: 0;
}

.ccm-02__hub:has(.ccm-02__state:focus-visible) .ccm-02__toggle {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

.ccm-02__orbit {
  position: absolute;
  inset: 0;
  list-style: none;
  --r: 0px;
  transition: --r .5s;
}

.ccm-02__item {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  --a: calc((360deg / var(--n)) * var(--i) - 90deg);
  translate: calc(cos(var(--a)) * var(--r)) calc(sin(var(--a)) * var(--r));
  opacity: 0;
  scale: .4;
  visibility: hidden;
  transition: translate .5s cubic-bezier(.34,1.35,.45,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.45,1),visibility 0s .5s;
  transition-delay: calc(var(--i) * 35ms);
}

.ccm-02__hub:has(:checked) .ccm-02__item {
  --r: 120px;
  opacity: 1;
  scale: 1;
  visibility: visible;
  transition: translate .5s cubic-bezier(.34,1.35,.45,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.45,1),visibility 0s;
  transition-delay: calc(var(--i) * 45ms);
}

.ccm-02__item a {
  pointer-events: auto;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: oklch(0.98 0.01 160 / .1);
  border: 1px solid oklch(0.9 0.05 160 / .25);
  backdrop-filter: blur(6px);
  color: oklch(0.92 0.04 160);
  transition: background .25s,color .25s,scale .25s;
}

.ccm-02__item a:hover,
.ccm-02__item a:focus-visible {
  background: var(--brand);
  color: var(--deep);
  scale: 1.12;
  outline: none;
}

.ccm-02__item svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ccm-02__note {
  position: absolute;
  bottom: 20px;
  left: 0;
  right: 0;
  text-align: center;
  font-size: 12.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: oklch(0.75 0.04 165 / .6);
}

@media (prefers-reduced-motion: reduce) {
  .ccm-02 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
```

How this works

The open state is a visually-hidden <input type="checkbox">; its <label> is the round trigger. The container styles descendants with :has(:checked), so the state check reads like a sentence: when my checkbox is checked, bloom.

.ccm-02__item{
  --a: calc((360deg / var(--n)) * var(--i) - 90deg);
  translate: calc(cos(var(--a)) * var(--r))
             calc(sin(var(--a)) * var(--r));
}

Subtracting 90deg puts item 0 at 12 o'clock. Because --n (count) and --r (radius) are tokens, adding a seventh link is one HTML edit — the circle redistributes itself.

Make it yours

  • Item count: set --n and add matching --i list items.
  • Half-circle variant: use calc(180deg / (var(--n) - 1) * var(--i) - 180deg).
  • Open on hover instead: swap :has(:checked) for :hover / :focus-within.
  • Ring radius is --r; the bloom easing lives in one transition rule.

Gotchas — read before shipping

  • Hide the checkbox with the visually-hidden clip pattern, not display:none — display:none kills keyboard toggling with Space.
  • Give the label :focus-visible styles via :has(:focus-visible) on the wrapper; the input holds focus, not the label.
  • Pure-CSS menus can't close on outside-click — acceptable for demos and kiosks, add 3 lines of JS for production apps.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Needs cos()/sin() (Chrome 111, Safari 15.4, Firefox 110) and :has() (Firefox 121). No fallback layout needed — without support the links simply stay stacked under the trigger.

Search CodeFronts

Loading…