SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/circular-menus

7 CSS Circular Menus

2 Pure CSS5 with JSPublished 2026-10-05Free · no sign-up

Seven circular menus: actions that spread round a button, a thumb-friendly semicircle, a pie with four wedge buttons, a clock-face hour picker, a quarter circle from the corner, a ring of team avatars and a dial that turns to the chosen setting.

01. Radial Menu Around a Button

CircularMenu

Pressing the centre button spreads five actions evenly round a circle. Each action's angle is its index times 72 degrees in a custom property, placed with rotate, translate and a counter-rotation so the icons stay upright. Closed actions are hidden and cannot be tabbed to.

View code
<div class="cm1"><ul id="cm1m" hidden><li style="--i:0"><button aria-label="Edit">✎</button></li><li style="--i:1"><button aria-label="Share">↗</button></li><li style="--i:2"><button aria-label="Copy">⧉</button></li><li style="--i:3"><button aria-label="Archive">▣</button></li><li style="--i:4"><button aria-label="Delete">✕</button></li></ul><button class="c" aria-expanded="false" aria-controls="cm1m" aria-label="Actions">+</button></div>
.cm1 { position: relative; width: 220px; height: 220px; display: grid; place-items: center; }
.cm1 ul { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; }
.cm1 li { position: absolute; left: 50%; top: 50%; transform: rotate(calc(var(--i) * 72deg - 90deg)) translate(80px) rotate(calc(var(--i) * -72deg + 90deg)); margin: -22px 0 0 -22px; animation: cm1 .3s cubic-bezier(.2,.8,.2,1) both; animation-delay: calc(var(--i) * 30ms); }
.cm1 li button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: #e0e7ff; color: #312e81; font-size: 18px; cursor: pointer; }
.cm1 .c { position: relative; z-index: 1; width: 64px; height: 64px; border: 0; border-radius: 50%; background: #6366f1; color: #fff; font-size: 30px; cursor: pointer; transition: transform .25s; }
.cm1 .c[aria-expanded=true] { transform: rotate(45deg); }
.cm1 button:focus-visible { outline: 3px solid #c7d2fe; outline-offset: 2px; }
@keyframes cm1 { from { transform: none; opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .cm1 li { animation: none; } .cm1 .c { transition: none; } }
var c = document.querySelector('.cm1 .c'), m = document.getElementById('cm1m');
c.onclick = function () { var o = c.getAttribute('aria-expanded') !== 'true'; c.setAttribute('aria-expanded', o); m.hidden = !o; };
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !m.hidden) { c.click(); c.focus(); } });

02. Semicircle Menu from the Bottom

CircularMobile

A thumb-friendly half circle of actions that opens above a button at the bottom of the screen. Four actions are spread over 180 degrees, which keeps every target within easy reach of the thumb holding the phone.

View code
<div class="cm2"><ul id="cm2m" hidden><li style="--a:-180deg"><button aria-label="Camera">📷</button></li><li style="--a:-120deg"><button aria-label="Gallery">🖼</button></li><li style="--a:-60deg"><button aria-label="Voice note">🎙</button></li><li style="--a:0deg"><button aria-label="File">📄</button></li></ul><button class="c" aria-expanded="false" aria-controls="cm2m" aria-label="Attach">📎</button></div>
.cm2 { position: relative; width: 240px; height: 160px; }
.cm2 ul { margin: 0; padding: 0; list-style: none; }
.cm2 li { position: absolute; left: 50%; bottom: 28px; margin-left: -22px; transform: rotate(var(--a)) translate(86px) rotate(calc(var(--a) * -1)); animation: cm2 .3s ease both; }
.cm2 li button { width: 44px; height: 44px; border: 1px solid #e4e7ec; border-radius: 50%; background: #fff; font-size: 18px; cursor: pointer; box-shadow: 0 4px 10px -4px rgba(16,24,40,.3); }
.cm2 .c { position: absolute; left: 50%; bottom: 0; margin-left: -28px; width: 56px; height: 56px; border: 0; border-radius: 50%; background: #101828; font-size: 22px; cursor: pointer; }
.cm2 button:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@keyframes cm2 { from { transform: none; opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .cm2 li { animation: none; } }
var c = document.querySelector('.cm2 .c'), m = document.getElementById('cm2m');
c.onclick = function () { var o = c.getAttribute('aria-expanded') !== 'true'; c.setAttribute('aria-expanded', o); m.hidden = !o; };

03. Four-Quadrant Pie Menu

CircularPie

A round control split into four wedge-shaped buttons, like a game controller's direction pad. Each wedge is a button clipped to a quarter circle with clip-path, with its label positioned towards its outer edge, and each has a full text name.

View code
<div class="cm3" role="group" aria-label="Move"><button class="n">Up</button><button class="e">Right</button><button class="s">Down</button><button class="w">Left</button><span class="mid" aria-hidden="true"></span></div>
.cm3 { position: relative; width: 180px; height: 180px; border-radius: 50%; font: 700 12px Inter, system-ui, sans-serif; }
.cm3 button { position: absolute; inset: 0; border: 0; border-radius: 50%; background: #1e293b; color: #e2e8f0; cursor: pointer; font: inherit; }
.cm3 button:hover { background: #334155; }
.cm3 .n { clip-path: polygon(50% 50%, 0 0, 100% 0); padding-bottom: 110px; }
.cm3 .e { clip-path: polygon(50% 50%, 100% 0, 100% 100%); padding-left: 110px; }
.cm3 .s { clip-path: polygon(50% 50%, 100% 100%, 0 100%); padding-top: 110px; }
.cm3 .w { clip-path: polygon(50% 50%, 0 100%, 0 0); padding-right: 110px; }
.cm3 button:focus-visible { background: #4338ca; outline: none; }
.cm3 .mid { position: absolute; inset: 60px; border-radius: 50%; background: #0e1118; box-shadow: 0 0 0 2px #334155; pointer-events: none; }

04. Clock-Face Hour Picker

CircularPicker

Twelve hour buttons arranged like a clock face, a natural way to pick a time on touch screens. They are radio buttons in a fieldset, so arrow keys move round the circle and only one hour can be chosen, and the choice is written in the centre.

View code
<fieldset class="cm4"><legend class="sr">Hour</legend><output aria-live="polite">9:00</output></fieldset>
.cm4 { position: relative; width: 200px; height: 200px; margin: 0; padding: 0; border: 0; border-radius: 50%; background: #f2f4f7; font: 600 13px Inter, system-ui, sans-serif; min-inline-size: 0; }
.cm4 label { position: absolute; left: 50%; top: 50%; width: 32px; height: 32px; margin: -16px 0 0 -16px; display: grid; place-items: center; border-radius: 50%; color: #344054; cursor: pointer; transform: rotate(calc(var(--h) * 30deg)) translateY(-78px) rotate(calc(var(--h) * -30deg)); }
.cm4 input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.cm4 label:has(:checked) { background: #101828; color: #fff; }
.cm4 label:has(:focus-visible) { outline: 3px solid #84adff; outline-offset: 1px; }
.cm4 output { position: absolute; inset: 0; display: grid; place-items: center; color: #101828; font-size: 20px; font-weight: 800; pointer-events: none; }
.cm4 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
var f = document.querySelector('.cm4'), out = f.querySelector('output');
for (var h = 1; h <= 12; h++) { var l = document.createElement('label'); l.style.setProperty('--h', h); l.innerHTML = '<input type="radio" name="cm4" value="' + h + '"' + (h === 9 ? ' checked' : '') + '>' + h; f.appendChild(l); }
f.addEventListener('change', function (e) { out.textContent = e.target.value + ':00'; });

05. Quarter-Circle Corner Menu

CircularCorner

A menu that opens as a quarter circle from the bottom-right corner, three links fanned across 90 degrees. It suits a corner of a game or creative tool, and the links are hidden from focus while it is closed.

View code
<div class="cm5"><ul id="cm5m" hidden><li style="--a:180deg"><a href="#new" aria-label="New">+</a></li><li style="--a:225deg"><a href="#open" aria-label="Open">⇪</a></li><li style="--a:270deg"><a href="#save" aria-label="Save">⤓</a></li></ul><button aria-expanded="false" aria-controls="cm5m" aria-label="File menu">☰</button></div>
.cm5 { position: relative; width: 200px; height: 200px; border-radius: 14px; background: #111727; overflow: hidden; }
.cm5 ul { margin: 0; padding: 0; list-style: none; }
.cm5 li { position: absolute; right: 16px; bottom: 16px; transform: rotate(var(--a)) translate(90px) rotate(calc(var(--a) * -1)); animation: cm5 .3s ease both; }
.cm5 a { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; background: #fbbf24; color: #1c1917; font-size: 20px; text-decoration: none; }
.cm5 button { position: absolute; right: 12px; bottom: 12px; width: 52px; height: 52px; border: 0; border-radius: 50%; background: #f8fafc; color: #0f172a; font-size: 20px; cursor: pointer; }
.cm5 a:focus-visible, .cm5 button:focus-visible { outline: 3px solid #fde68a; outline-offset: 2px; }
@keyframes cm5 { from { transform: none; opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .cm5 li { animation: none; } }
var b = document.querySelector('.cm5 button'), m = document.getElementById('cm5m');
b.onclick = function () { var o = b.getAttribute('aria-expanded') !== 'true'; b.setAttribute('aria-expanded', o); m.hidden = !o; };

06. Team Circle of Avatars

CircularAvatars

Team members arranged in a ring around the company logo, each avatar a link to their profile with their name as its accessible name. The ring positions come from the same rotate and translate trick as a radial menu, with no JavaScript.

View code
<div class="cm6"><span class="logo" aria-hidden="true">K&amp;C</span><a href="#ana" style="--i:0" aria-label="Ana, founder">AN</a><a href="#ben" style="--i:1" aria-label="Ben, ceramicist">BE</a><a href="#cai" style="--i:2" aria-label="Cai, glazes">CA</a><a href="#dee" style="--i:3" aria-label="Dee, shop">DE</a><a href="#eli" style="--i:4" aria-label="Eli, packing">EL</a><a href="#fay" style="--i:5" aria-label="Fay, marketing">FA</a></div>
.cm6 { position: relative; width: 220px; height: 220px; display: grid; place-items: center; font: 800 13px Inter, system-ui, sans-serif; }
.cm6 .logo { display: grid; place-items: center; width: 70px; height: 70px; border-radius: 50%; background: #101828; color: #fff; }
.cm6 a { position: absolute; left: 50%; top: 50%; width: 44px; height: 44px; margin: -22px 0 0 -22px; display: grid; place-items: center; border: 3px solid #fff; border-radius: 50%; background: hsl(calc(var(--i) * 60) 80% 85%); color: #101828; text-decoration: none; transform: rotate(calc(var(--i) * 60deg)) translate(82px) rotate(calc(var(--i) * -60deg)); box-shadow: 0 2px 6px rgba(16,24,40,.2); }
.cm6 a:hover { z-index: 1; box-shadow: 0 0 0 3px #101828; }
.cm6 a:focus-visible { outline: 3px solid #528bff; outline-offset: 2px; }

07. Rotating Dial Selector

CircularDial

A dial that rotates to point at the chosen mode, built on a native range input so arrow keys and screen readers work. The input's value sets the dial's rotation and the mode name below it, and the dial graphic is decoration only.

View code
<div class="cm7"><div class="dial" aria-hidden="true"><span class="ptr"></span></div><ul class="lbl" aria-hidden="true"><li style="--i:0">Off</li><li style="--i:1">Low</li><li style="--i:2">Med</li><li style="--i:3">High</li></ul><label for="cm7r">Fan speed: <b>Off</b></label><input id="cm7r" type="range" min="0" max="3" value="0" aria-valuetext="Off"></div>
.cm7 { position: relative; display: grid; justify-items: center; gap: 10px; width: 220px; font: 13px Inter, system-ui, sans-serif; color: #cbd5e1; }
.cm7 .dial { --r: -135deg; position: relative; width: 110px; height: 110px; margin-top: 30px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, #475569, #1e293b); box-shadow: inset 0 -4px 8px rgba(0,0,0,.4), 0 6px 14px -6px #000; transform: rotate(var(--r)); transition: transform .3s cubic-bezier(.2,.8,.2,1); }
.cm7 .ptr { position: absolute; left: 50%; top: 8px; width: 4px; height: 22px; margin-left: -2px; border-radius: 2px; background: #38bdf8; }
.cm7 .lbl { position: absolute; top: 0; left: 50%; width: 0; height: 0; margin: 0; padding: 0; list-style: none; }
.cm7 .lbl li { position: absolute; left: 0; top: 85px; width: 40px; margin-left: -20px; text-align: center; font-size: 11px; transform: rotate(calc(-135deg + var(--i) * 90deg)) translateY(-78px) rotate(calc(135deg - var(--i) * 90deg)); }
.cm7 b { color: #fff; }
.cm7 input { width: 160px; accent-color: #38bdf8; }
.cm7 input:focus-visible { outline: 2px solid #38bdf8; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .cm7 .dial { transition: none; } }
var r = document.getElementById('cm7r'), d = document.querySelector('.cm7 .dial'), b = document.querySelector('.cm7 b'), names = ['Off', 'Low', 'Med', 'High'];
r.oninput = function () { d.style.setProperty('--r', (-135 + r.value * 90) + 'deg'); b.textContent = names[r.value]; r.setAttribute('aria-valuetext', names[r.value]); };

About these CSS Circular Menus

The placement trick

rotate(angle) translate(radius) rotate(-angle) places an item on a circle and turns it back upright. Store the angle in a custom property per item, such as --i multiplied by the step, and the whole ring is CSS.

Hidden means unreachable

When a radial menu is closed, hide its list with the hidden attribute, not just opacity, so its buttons cannot be tabbed to. The toggle reports the state with aria-expanded.

Use native controls where possible

A clock face is a set of radio buttons and a dial is a range input. Laying familiar controls out in a circle keeps keyboard and screen reader support for free.

Keep targets large

Items on a circle crowd quickly. Keep each target at least 44 pixels and limit the ring to five to eight items.

Frequently asked questions

Are radial menus good on desktop?

They suit touch, games and creative tools. For ordinary websites a list is faster to read.

How do I change the radius?

Change the translate distance; keep it larger than half the button size plus the item size.

Do they need JavaScript?

Only to toggle open and closed. The layout is pure CSS.