14 / 26
All 26 in 26 CSS Circular Menus
- 01Floating Action Radial Menu (FAB Speed Dial)
- 02Pure CSS Radial Submenu (sin() & cos())
- 03Interactive Wheel Menu (Pie Slice Dropdown)
- 04SVG Gooey Circular Navigation Menu
- 05Corner Arc Navigation (90-Degree Expansion)
- 06Responsive Dashboard Radial Hub
- 07Circular Service Ring Navigation
- 08Rotary Control Dial Interface
- 09Collapsible Fan-Out Radial Menu
- 10Petal Reveal Flower Navigation
- 11Full 360-Degree Wheel Navigation
- 12Pie Chart Navigation Menu
- 13Donut Segment Radial Dropdown
- 14Semi-Circle Speed Dial Menu
- 15Glassmorphism Circular Popover Menu
- 16Glowing Cyberpunk Radial UI
- 17Single Satellite Orbiting Menu Toggle
- 18Solar System Radial Submenu Navigation
- 19Hexagonal Grid Radial Selector
- 20Triangular Rotary Navigation Switcher
- 21Retro Vinyl Record Rotating Menu
- 22Compass Rose Directional Navigation
- 23Camera Iris Aperture Expand Menu
- 24Vortex / Particle Expand Menu
- 25Clockface Chronometer Circular Selector
- 26Blurred Particle Radial Overlay
Semi-Circle Speed Dial Menu
A bottom-docked half-circle dial made for thumbs: five actions arc across 180° above a center trigger, exactly spanning the natural sweep of a thumb on a phone. The half-disc backdrop rises like a tiny bottom sheet. This is the radial pattern that actually belongs on mobile.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="ccm-14" aria-label="Semi-circle speed dial demo">
<div class="ccm-14__stage">
<div class="ccm-14__phone">
<div class="ccm-14__screen" aria-hidden="true"><span></span><span style="width:64%"></span><span style="width:78%"></span></div>
<div class="ccm-14__dock" style="--n:5">
<span class="ccm-14__sheet" aria-hidden="true"></span>
<ul class="ccm-14__arc" id="ccm14-menu">
<li style="--i:0"><button type="button" aria-label="Camera"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.2"/></svg></button></li>
<li style="--i:1"><button type="button" aria-label="Gallery"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M4 15l4-4 4 4 3-3 5 5"/><circle cx="9" cy="9" r="1.2"/></svg></button></li>
<li style="--i:2"><button type="button" aria-label="New post"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M6 12h12"/></svg></button></li>
<li style="--i:3"><button type="button" aria-label="Voice note"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM6 11a6 6 0 0 0 12 0M12 17v4"/></svg></button></li>
<li style="--i:4"><button type="button" aria-label="Live"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M6.3 6.3a8 8 0 0 0 0 11.4M17.7 6.3a8 8 0 0 1 0 11.4" transform="rotate(90 12 12)"/></svg></button></li>
</ul>
<button type="button" class="ccm-14__go" aria-expanded="false" aria-controls="ccm14-menu" aria-label="Open create menu"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
</div>
</div>
</section><section class="ccm-14" aria-label="Semi-circle speed dial demo">
<div class="ccm-14__stage">
<div class="ccm-14__phone">
<div class="ccm-14__screen" aria-hidden="true"><span></span><span style="width:64%"></span><span style="width:78%"></span></div>
<div class="ccm-14__dock" style="--n:5">
<span class="ccm-14__sheet" aria-hidden="true"></span>
<ul class="ccm-14__arc" id="ccm14-menu">
<li style="--i:0"><button type="button" aria-label="Camera"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.2"/></svg></button></li>
<li style="--i:1"><button type="button" aria-label="Gallery"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M4 15l4-4 4 4 3-3 5 5"/><circle cx="9" cy="9" r="1.2"/></svg></button></li>
<li style="--i:2"><button type="button" aria-label="New post"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M6 12h12"/></svg></button></li>
<li style="--i:3"><button type="button" aria-label="Voice note"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM6 11a6 6 0 0 0 12 0M12 17v4"/></svg></button></li>
<li style="--i:4"><button type="button" aria-label="Live"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M6.3 6.3a8 8 0 0 0 0 11.4M17.7 6.3a8 8 0 0 1 0 11.4" transform="rotate(90 12 12)"/></svg></button></li>
</ul>
<button type="button" class="ccm-14__go" aria-expanded="false" aria-controls="ccm14-menu" aria-label="Open create menu"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
</div>
</div>
</section>.ccm-14,
.ccm-14 *,
.ccm-14 *::before,
.ccm-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-14 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
--brand: oklch(0.65 0.21 300);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.ccm-14__stage {
width: 100%;
height: 100vh;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
border: 1px solid oklch(0.87 0.02 295);
padding: 30px;
}
.ccm-14__phone {
position: relative;
width: 300px;
height: 460px;
border-radius: 36px;
background: oklch(0.99 0.002 300);
border: 1px solid oklch(0.88 0.015 300);
box-shadow: 0 30px 60px -30px oklch(0.4 0.1 295 / .6);
overflow: hidden;
}
.ccm-14__screen {
display: grid;
gap: 12px;
padding: 28px 24px;
}
.ccm-14__screen span {
display: block;
height: 12px;
border-radius: 6px;
background: oklch(0.93 0.01 300);
}
.ccm-14__screen span:first-child {
height: 22px;
width: 48%;
background: oklch(0.89 0.02 300);
}
.ccm-14__dock {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 210px;
pointer-events: none;
}
.ccm-14__dock > * {
pointer-events: auto;
}
.ccm-14__sheet {
position: absolute;
left: 50%;
bottom: -150px;
width: 420px;
height: 420px;
margin-left: -210px;
border-radius: 50%;
background: linear-gradient(180deg,color-mix(in oklab,var(--brand) 96%,black),color-mix(in oklab,var(--brand) 75%,black));
box-shadow: 0 -14px 44px -14px oklch(0.5 0.2 300 / .55);
translate: 0 105%;
transition: translate .5s cubic-bezier(.32,1.15,.35,1);
pointer-events: none;
}
.ccm-14__dock.is-open .ccm-14__sheet {
translate: 0 0;
}
.ccm-14__arc {
position: absolute;
left: 50%;
bottom: 44px;
list-style: none;
}
.ccm-14__arc li {
position: absolute;
left: 0;
bottom: 0;
--a: calc(180deg + (180deg / (var(--n) - 1)) * var(--i));
}
.ccm-14__arc button {
position: absolute;
left: -24px;
bottom: -24px;
display: grid;
place-items: center;
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
background: oklch(1 0 0 / .14);
color: #fff;
cursor: pointer;
translate: 0 0;
opacity: 0;
scale: .4;
visibility: hidden;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .25s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s .5s,background .2s;
transition-delay: calc(var(--i) * 35ms);
}
.ccm-14__dock.is-open .ccm-14__arc button {
translate: calc(cos(var(--a)) * 108px) calc(sin(var(--a)) * 108px);
opacity: 1;
scale: 1;
visibility: visible;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s,background .2s;
transition-delay: calc(var(--i) * 45ms);
}
.ccm-14__arc button:hover,
.ccm-14__arc button:focus-visible {
background: oklch(1 0 0 / .3);
}
.ccm-14__arc button:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.ccm-14__arc svg {
width: 21px;
height: 21px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.ccm-14__go {
position: absolute;
left: 50%;
bottom: 22px;
translate: -50% 0;
display: grid;
place-items: center;
width: 58px;
height: 58px;
border: none;
border-radius: 50%;
background: var(--brand);
color: #fff;
cursor: pointer;
box-shadow: 0 12px 30px -10px oklch(0.55 0.21 300 / .7);
transition: rotate .4s cubic-bezier(.34,1.35,.4,1),background .3s;
z-index: 2;
}
.ccm-14__go svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
}
.ccm-14__go:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 3px;
}
.ccm-14__dock.is-open .ccm-14__go {
rotate: 135deg;
background: oklch(1 0 0 / .18);
}
@media (prefers-reduced-motion: reduce) {
.ccm-14 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}.ccm-14,
.ccm-14 *,
.ccm-14 *::before,
.ccm-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-14 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
--brand: oklch(0.65 0.21 300);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.ccm-14__stage {
width: 100%;
height: 100vh;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
border: 1px solid oklch(0.87 0.02 295);
padding: 30px;
}
.ccm-14__phone {
position: relative;
width: 300px;
height: 460px;
border-radius: 36px;
background: oklch(0.99 0.002 300);
border: 1px solid oklch(0.88 0.015 300);
box-shadow: 0 30px 60px -30px oklch(0.4 0.1 295 / .6);
overflow: hidden;
}
.ccm-14__screen {
display: grid;
gap: 12px;
padding: 28px 24px;
}
.ccm-14__screen span {
display: block;
height: 12px;
border-radius: 6px;
background: oklch(0.93 0.01 300);
}
.ccm-14__screen span:first-child {
height: 22px;
width: 48%;
background: oklch(0.89 0.02 300);
}
.ccm-14__dock {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 210px;
pointer-events: none;
}
.ccm-14__dock > * {
pointer-events: auto;
}
.ccm-14__sheet {
position: absolute;
left: 50%;
bottom: -150px;
width: 420px;
height: 420px;
margin-left: -210px;
border-radius: 50%;
background: linear-gradient(180deg,color-mix(in oklab,var(--brand) 96%,black),color-mix(in oklab,var(--brand) 75%,black));
box-shadow: 0 -14px 44px -14px oklch(0.5 0.2 300 / .55);
translate: 0 105%;
transition: translate .5s cubic-bezier(.32,1.15,.35,1);
pointer-events: none;
}
.ccm-14__dock.is-open .ccm-14__sheet {
translate: 0 0;
}
.ccm-14__arc {
position: absolute;
left: 50%;
bottom: 44px;
list-style: none;
}
.ccm-14__arc li {
position: absolute;
left: 0;
bottom: 0;
--a: calc(180deg + (180deg / (var(--n) - 1)) * var(--i));
}
.ccm-14__arc button {
position: absolute;
left: -24px;
bottom: -24px;
display: grid;
place-items: center;
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
background: oklch(1 0 0 / .14);
color: #fff;
cursor: pointer;
translate: 0 0;
opacity: 0;
scale: .4;
visibility: hidden;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .25s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s .5s,background .2s;
transition-delay: calc(var(--i) * 35ms);
}
.ccm-14__dock.is-open .ccm-14__arc button {
translate: calc(cos(var(--a)) * 108px) calc(sin(var(--a)) * 108px);
opacity: 1;
scale: 1;
visibility: visible;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s,background .2s;
transition-delay: calc(var(--i) * 45ms);
}
.ccm-14__arc button:hover,
.ccm-14__arc button:focus-visible {
background: oklch(1 0 0 / .3);
}
.ccm-14__arc button:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.ccm-14__arc svg {
width: 21px;
height: 21px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.ccm-14__go {
position: absolute;
left: 50%;
bottom: 22px;
translate: -50% 0;
display: grid;
place-items: center;
width: 58px;
height: 58px;
border: none;
border-radius: 50%;
background: var(--brand);
color: #fff;
cursor: pointer;
box-shadow: 0 12px 30px -10px oklch(0.55 0.21 300 / .7);
transition: rotate .4s cubic-bezier(.34,1.35,.4,1),background .3s;
z-index: 2;
}
.ccm-14__go svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
}
.ccm-14__go:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 3px;
}
.ccm-14__dock.is-open .ccm-14__go {
rotate: 135deg;
background: oklch(1 0 0 / .18);
}
@media (prefers-reduced-motion: reduce) {
.ccm-14 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}(() => {
const dock = document.querySelector('.ccm-14__dock');
const go = dock.querySelector('.ccm-14__go');
const set = (open) => {
dock.classList.toggle('is-open', open);
go.setAttribute('aria-expanded', String(open));
};
go.addEventListener('click', () => set(!dock.classList.contains('is-open')));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dock.classList.contains('is-open')) { set(false); go.focus(); }
});
})();(() => {
const dock = document.querySelector('.ccm-14__dock');
const go = dock.querySelector('.ccm-14__go');
const set = (open) => {
dock.classList.toggle('is-open', open);
go.setAttribute('aria-expanded', String(open));
};
go.addEventListener('click', () => set(!dock.classList.contains('is-open')));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dock.classList.contains('is-open')) { set(false); go.focus(); }
});
})();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: Semi-Circle Speed Dial Menu
Source: https://codefronts.com/navigation/css-circular-menus/half-donut-speed-dial/
A bottom-docked half-circle dial made for thumbs: five actions arc across 180° above a center trigger, exactly spanning the natural sweep of a thumb on a phone. The half-disc backdrop rises like a tiny bottom sheet. This is the radial pattern that actually belongs on mobile.
## HTML
```html
<section class="ccm-14" aria-label="Semi-circle speed dial demo">
<div class="ccm-14__stage">
<div class="ccm-14__phone">
<div class="ccm-14__screen" aria-hidden="true"><span></span><span style="width:64%"></span><span style="width:78%"></span></div>
<div class="ccm-14__dock" style="--n:5">
<span class="ccm-14__sheet" aria-hidden="true"></span>
<ul class="ccm-14__arc" id="ccm14-menu">
<li style="--i:0"><button type="button" aria-label="Camera"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.2"/></svg></button></li>
<li style="--i:1"><button type="button" aria-label="Gallery"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M4 15l4-4 4 4 3-3 5 5"/><circle cx="9" cy="9" r="1.2"/></svg></button></li>
<li style="--i:2"><button type="button" aria-label="New post"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M6 12h12"/></svg></button></li>
<li style="--i:3"><button type="button" aria-label="Voice note"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM6 11a6 6 0 0 0 12 0M12 17v4"/></svg></button></li>
<li style="--i:4"><button type="button" aria-label="Live"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M6.3 6.3a8 8 0 0 0 0 11.4M17.7 6.3a8 8 0 0 1 0 11.4" transform="rotate(90 12 12)"/></svg></button></li>
</ul>
<button type="button" class="ccm-14__go" aria-expanded="false" aria-controls="ccm14-menu" aria-label="Open create menu"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
</div>
</div>
</section>
```
## CSS
```css
.ccm-14,
.ccm-14 *,
.ccm-14 *::before,
.ccm-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-14 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
--brand: oklch(0.65 0.21 300);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.ccm-14__stage {
width: 100%;
height: 100vh;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
border: 1px solid oklch(0.87 0.02 295);
padding: 30px;
}
.ccm-14__phone {
position: relative;
width: 300px;
height: 460px;
border-radius: 36px;
background: oklch(0.99 0.002 300);
border: 1px solid oklch(0.88 0.015 300);
box-shadow: 0 30px 60px -30px oklch(0.4 0.1 295 / .6);
overflow: hidden;
}
.ccm-14__screen {
display: grid;
gap: 12px;
padding: 28px 24px;
}
.ccm-14__screen span {
display: block;
height: 12px;
border-radius: 6px;
background: oklch(0.93 0.01 300);
}
.ccm-14__screen span:first-child {
height: 22px;
width: 48%;
background: oklch(0.89 0.02 300);
}
.ccm-14__dock {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 210px;
pointer-events: none;
}
.ccm-14__dock > * {
pointer-events: auto;
}
.ccm-14__sheet {
position: absolute;
left: 50%;
bottom: -150px;
width: 420px;
height: 420px;
margin-left: -210px;
border-radius: 50%;
background: linear-gradient(180deg,color-mix(in oklab,var(--brand) 96%,black),color-mix(in oklab,var(--brand) 75%,black));
box-shadow: 0 -14px 44px -14px oklch(0.5 0.2 300 / .55);
translate: 0 105%;
transition: translate .5s cubic-bezier(.32,1.15,.35,1);
pointer-events: none;
}
.ccm-14__dock.is-open .ccm-14__sheet {
translate: 0 0;
}
.ccm-14__arc {
position: absolute;
left: 50%;
bottom: 44px;
list-style: none;
}
.ccm-14__arc li {
position: absolute;
left: 0;
bottom: 0;
--a: calc(180deg + (180deg / (var(--n) - 1)) * var(--i));
}
.ccm-14__arc button {
position: absolute;
left: -24px;
bottom: -24px;
display: grid;
place-items: center;
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
background: oklch(1 0 0 / .14);
color: #fff;
cursor: pointer;
translate: 0 0;
opacity: 0;
scale: .4;
visibility: hidden;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .25s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s .5s,background .2s;
transition-delay: calc(var(--i) * 35ms);
}
.ccm-14__dock.is-open .ccm-14__arc button {
translate: calc(cos(var(--a)) * 108px) calc(sin(var(--a)) * 108px);
opacity: 1;
scale: 1;
visibility: visible;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s,background .2s;
transition-delay: calc(var(--i) * 45ms);
}
.ccm-14__arc button:hover,
.ccm-14__arc button:focus-visible {
background: oklch(1 0 0 / .3);
}
.ccm-14__arc button:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.ccm-14__arc svg {
width: 21px;
height: 21px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.ccm-14__go {
position: absolute;
left: 50%;
bottom: 22px;
translate: -50% 0;
display: grid;
place-items: center;
width: 58px;
height: 58px;
border: none;
border-radius: 50%;
background: var(--brand);
color: #fff;
cursor: pointer;
box-shadow: 0 12px 30px -10px oklch(0.55 0.21 300 / .7);
transition: rotate .4s cubic-bezier(.34,1.35,.4,1),background .3s;
z-index: 2;
}
.ccm-14__go svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
}
.ccm-14__go:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 3px;
}
.ccm-14__dock.is-open .ccm-14__go {
rotate: 135deg;
background: oklch(1 0 0 / .18);
}
@media (prefers-reduced-motion: reduce) {
.ccm-14 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}
```
## JavaScript
```js
(() => {
const dock = document.querySelector('.ccm-14__dock');
const go = dock.querySelector('.ccm-14__go');
const set = (open) => {
dock.classList.toggle('is-open', open);
go.setAttribute('aria-expanded', String(open));
};
go.addEventListener('click', () => set(!dock.classList.contains('is-open')));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dock.classList.contains('is-open')) { set(false); go.focus(); }
});
})();
```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: Semi-Circle Speed Dial Menu
Source: https://codefronts.com/navigation/css-circular-menus/half-donut-speed-dial/
A bottom-docked half-circle dial made for thumbs: five actions arc across 180° above a center trigger, exactly spanning the natural sweep of a thumb on a phone. The half-disc backdrop rises like a tiny bottom sheet. This is the radial pattern that actually belongs on mobile.
## HTML
```html
<section class="ccm-14" aria-label="Semi-circle speed dial demo">
<div class="ccm-14__stage">
<div class="ccm-14__phone">
<div class="ccm-14__screen" aria-hidden="true"><span></span><span style="width:64%"></span><span style="width:78%"></span></div>
<div class="ccm-14__dock" style="--n:5">
<span class="ccm-14__sheet" aria-hidden="true"></span>
<ul class="ccm-14__arc" id="ccm14-menu">
<li style="--i:0"><button type="button" aria-label="Camera"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.2"/></svg></button></li>
<li style="--i:1"><button type="button" aria-label="Gallery"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M4 15l4-4 4 4 3-3 5 5"/><circle cx="9" cy="9" r="1.2"/></svg></button></li>
<li style="--i:2"><button type="button" aria-label="New post"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M6 12h12"/></svg></button></li>
<li style="--i:3"><button type="button" aria-label="Voice note"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM6 11a6 6 0 0 0 12 0M12 17v4"/></svg></button></li>
<li style="--i:4"><button type="button" aria-label="Live"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M6.3 6.3a8 8 0 0 0 0 11.4M17.7 6.3a8 8 0 0 1 0 11.4" transform="rotate(90 12 12)"/></svg></button></li>
</ul>
<button type="button" class="ccm-14__go" aria-expanded="false" aria-controls="ccm14-menu" aria-label="Open create menu"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
</div>
</div>
</section>
```
## CSS
```css
.ccm-14,
.ccm-14 *,
.ccm-14 *::before,
.ccm-14 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-14 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
--brand: oklch(0.65 0.21 300);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.ccm-14__stage {
width: 100%;
height: 100vh;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(170deg,oklch(0.93 0.02 300),oklch(0.88 0.035 290));
border: 1px solid oklch(0.87 0.02 295);
padding: 30px;
}
.ccm-14__phone {
position: relative;
width: 300px;
height: 460px;
border-radius: 36px;
background: oklch(0.99 0.002 300);
border: 1px solid oklch(0.88 0.015 300);
box-shadow: 0 30px 60px -30px oklch(0.4 0.1 295 / .6);
overflow: hidden;
}
.ccm-14__screen {
display: grid;
gap: 12px;
padding: 28px 24px;
}
.ccm-14__screen span {
display: block;
height: 12px;
border-radius: 6px;
background: oklch(0.93 0.01 300);
}
.ccm-14__screen span:first-child {
height: 22px;
width: 48%;
background: oklch(0.89 0.02 300);
}
.ccm-14__dock {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 210px;
pointer-events: none;
}
.ccm-14__dock > * {
pointer-events: auto;
}
.ccm-14__sheet {
position: absolute;
left: 50%;
bottom: -150px;
width: 420px;
height: 420px;
margin-left: -210px;
border-radius: 50%;
background: linear-gradient(180deg,color-mix(in oklab,var(--brand) 96%,black),color-mix(in oklab,var(--brand) 75%,black));
box-shadow: 0 -14px 44px -14px oklch(0.5 0.2 300 / .55);
translate: 0 105%;
transition: translate .5s cubic-bezier(.32,1.15,.35,1);
pointer-events: none;
}
.ccm-14__dock.is-open .ccm-14__sheet {
translate: 0 0;
}
.ccm-14__arc {
position: absolute;
left: 50%;
bottom: 44px;
list-style: none;
}
.ccm-14__arc li {
position: absolute;
left: 0;
bottom: 0;
--a: calc(180deg + (180deg / (var(--n) - 1)) * var(--i));
}
.ccm-14__arc button {
position: absolute;
left: -24px;
bottom: -24px;
display: grid;
place-items: center;
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
background: oklch(1 0 0 / .14);
color: #fff;
cursor: pointer;
translate: 0 0;
opacity: 0;
scale: .4;
visibility: hidden;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .25s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s .5s,background .2s;
transition-delay: calc(var(--i) * 35ms);
}
.ccm-14__dock.is-open .ccm-14__arc button {
translate: calc(cos(var(--a)) * 108px) calc(sin(var(--a)) * 108px);
opacity: 1;
scale: 1;
visibility: visible;
transition: translate .5s cubic-bezier(.34,1.35,.4,1),opacity .3s,scale .5s cubic-bezier(.34,1.35,.4,1),visibility 0s,background .2s;
transition-delay: calc(var(--i) * 45ms);
}
.ccm-14__arc button:hover,
.ccm-14__arc button:focus-visible {
background: oklch(1 0 0 / .3);
}
.ccm-14__arc button:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.ccm-14__arc svg {
width: 21px;
height: 21px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.ccm-14__go {
position: absolute;
left: 50%;
bottom: 22px;
translate: -50% 0;
display: grid;
place-items: center;
width: 58px;
height: 58px;
border: none;
border-radius: 50%;
background: var(--brand);
color: #fff;
cursor: pointer;
box-shadow: 0 12px 30px -10px oklch(0.55 0.21 300 / .7);
transition: rotate .4s cubic-bezier(.34,1.35,.4,1),background .3s;
z-index: 2;
}
.ccm-14__go svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
}
.ccm-14__go:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 3px;
}
.ccm-14__dock.is-open .ccm-14__go {
rotate: 135deg;
background: oklch(1 0 0 / .18);
}
@media (prefers-reduced-motion: reduce) {
.ccm-14 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}
```
## JavaScript
```js
(() => {
const dock = document.querySelector('.ccm-14__dock');
const go = dock.querySelector('.ccm-14__go');
const set = (open) => {
dock.classList.toggle('is-open', open);
go.setAttribute('aria-expanded', String(open));
};
go.addEventListener('click', () => set(!dock.classList.contains('is-open')));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dock.classList.contains('is-open')) { set(false); go.focus(); }
});
})();
```How this works
Items spread across the top half only: --a: calc(180deg + (180deg / (var(--n) - 1)) * var(--i)) walks from 180° (left) through 270° (up) to 360° (right). Dividing by n − 1 instead of n puts items at both endpoints of the arc — the fence-post detail that half-circle menus get wrong when copied from full-circle code.
The backdrop is a plain element with border-radius: 50% 50% 0 0 / 100% 100% 0 0 — an elliptical top edge — sliding up via translate. Item motion, stagger, and a11y wiring are identical to the FAB pattern, so you can read the two demos side by side and see exactly what changes between 90°, 180° and 360° variants: only the angle formula.
Make it yours
- Arc span: swap
180degfor120degfor a tighter crown. - Item count:
--n— the fence-post formula redistributes automatically. - The half-disc can carry a blur (
backdrop-filter) to double as a scrim. - Center it in a toolbar or dock it to a screen edge — anchor math is corner-free.
Gotchas — read before shipping
- Use
n − 1in the angle step or your endpoints float off the horizon line. - Dock with safe-area padding on real phones:
padding-bottom: env(safe-area-inset-bottom). - The rising backdrop must be
pointer-events:nonewhen closed or it blocks taps on content above.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 110+ | 111+ |
Same trig baseline as the other cos()/sin() demos; the elliptical border-radius backdrop works everywhere.