26 CSS Circular Menus19 / 26
Hexagonal Grid Radial Selector
A honeycomb selector: six hexagonal cells lock around a central hex like real comb, each cut with one clip-path and placed with the same ring trig as every circle menu — hexagons ARE a radial layout with a 60° step. Radio-group semantics underneath; the picked cell fills with the accent and pops forward. No JavaScript.
Published Updated
The code
<section class="ccm-19" aria-label="Hexagonal grid selector demo">
<div class="ccm-19__stage">
<fieldset class="ccm-19__comb" style="--n:6"><legend class="ccm-19__sr">Choose a class</legend>
<div class="ccm-19__center"><span class="ccm-19__hexface"><em>PICK</em><output class="ccm-19__out">Ranger</output></span></div>
<div class="ccm-19__cell" style="--i:0"><input type="radio" name="ccm19" id="ccm19-a" value="Ranger" checked><label for="ccm19-a"><b>🏹</b>Ranger</label></div>
<div class="ccm-19__cell" style="--i:1"><input type="radio" name="ccm19" id="ccm19-b" value="Mage"><label for="ccm19-b"><b>✦</b>Mage</label></div>
<div class="ccm-19__cell" style="--i:2"><input type="radio" name="ccm19" id="ccm19-c" value="Knight"><label for="ccm19-c"><b>🛡</b>Knight</label></div>
<div class="ccm-19__cell" style="--i:3"><input type="radio" name="ccm19" id="ccm19-d" value="Rogue"><label for="ccm19-d"><b>🗡</b>Rogue</label></div>
<div class="ccm-19__cell" style="--i:4"><input type="radio" name="ccm19" id="ccm19-e" value="Cleric"><label for="ccm19-e"><b>☥</b>Cleric</label></div>
<div class="ccm-19__cell" style="--i:5"><input type="radio" name="ccm19" id="ccm19-f" value="Bard"><label for="ccm19-f"><b>♪</b>Bard</label></div>
</fieldset>
<p class="ccm-19__note">Pure CSS · arrow keys cycle the comb</p>
</div>
</section><section class="ccm-19" aria-label="Hexagonal grid selector demo">
<div class="ccm-19__stage">
<fieldset class="ccm-19__comb" style="--n:6"><legend class="ccm-19__sr">Choose a class</legend>
<div class="ccm-19__center"><span class="ccm-19__hexface"><em>PICK</em><output class="ccm-19__out">Ranger</output></span></div>
<div class="ccm-19__cell" style="--i:0"><input type="radio" name="ccm19" id="ccm19-a" value="Ranger" checked><label for="ccm19-a"><b>🏹</b>Ranger</label></div>
<div class="ccm-19__cell" style="--i:1"><input type="radio" name="ccm19" id="ccm19-b" value="Mage"><label for="ccm19-b"><b>✦</b>Mage</label></div>
<div class="ccm-19__cell" style="--i:2"><input type="radio" name="ccm19" id="ccm19-c" value="Knight"><label for="ccm19-c"><b>🛡</b>Knight</label></div>
<div class="ccm-19__cell" style="--i:3"><input type="radio" name="ccm19" id="ccm19-d" value="Rogue"><label for="ccm19-d"><b>🗡</b>Rogue</label></div>
<div class="ccm-19__cell" style="--i:4"><input type="radio" name="ccm19" id="ccm19-e" value="Cleric"><label for="ccm19-e"><b>☥</b>Cleric</label></div>
<div class="ccm-19__cell" style="--i:5"><input type="radio" name="ccm19" id="ccm19-f" value="Bard"><label for="ccm19-f"><b>♪</b>Bard</label></div>
</fieldset>
<p class="ccm-19__note">Pure CSS · arrow keys cycle the comb</p>
</div>
</section>.ccm-19,
.ccm-19 *,
.ccm-19 *::before,
.ccm-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-19 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
--brand: oklch(0.78 0.15 80);
--hex: polygon(25% 3.35%,75% 3.35%,100% 50%,75% 96.65%,25% 96.65%,0% 50%);
--cell: 104px;
font-family: 'Segoe UI',system-ui,sans-serif;
color: oklch(0.9 0.01 80);
}
.ccm-19__stage {
width: 100%;
height: 100vh;
position: relative;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
overflow: hidden;
}
.ccm-19__comb {
position: relative;
border: none;
width: calc(var(--cell) * 3.1);
height: calc(var(--cell) * 3.1);
display: grid;
place-items: center;
}
.ccm-19__sr {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.ccm-19__center,
.ccm-19__cell {
position: absolute;
left: 50%;
top: 50%;
width: var(--cell);
height: calc(var(--cell) * 1.1547);
}
.ccm-19__center {
translate: -50% -50%;
display: grid;
}
.ccm-19__hexface {
display: grid;
place-content: center;
justify-items: center;
gap: 2px;
clip-path: var(--hex);
background: radial-gradient(circle at 40% 30%,oklch(0.34 0.03 65),oklch(0.24 0.02 55));
text-align: center;
}
.ccm-19__hexface em {
font-style: normal;
font-size: 9.5px;
letter-spacing: .3em;
color: oklch(0.62 0.06 75);
}
.ccm-19__out {
font-size: 16.5px;
font-weight: 800;
color: var(--brand);
}
.ccm-19__cell {
--a: calc(60deg * var(--i) - 90deg);
--d: calc(var(--cell) * 1.08);
translate: calc(cos(var(--a)) * var(--d) - 50%) calc(sin(var(--a)) * var(--d) - 50%);
transition: scale .25s cubic-bezier(.34,1.35,.4,1),filter .25s;
/* second ring: 12 cells alternating --d of 2.16 and 1.87 × cell */
}
.ccm-19__cell:has(:checked) {
scale: 1.08;
z-index: 1;
filter: drop-shadow(0 8px 18px oklch(0.7 0.15 80 / .45));
}
.ccm-19__cell input {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}
.ccm-19__cell label {
position: absolute;
inset: 0;
clip-path: var(--hex);
display: grid;
place-content: center;
justify-items: center;
gap: 5px;
background: oklch(0.3 0.025 60);
border: none;
cursor: pointer;
font-size: 12.5px;
font-weight: 700;
letter-spacing: .03em;
transition: background .25s,color .25s;
}
.ccm-19__cell label b {
font-size: 21px;
font-weight: 400;
line-height: 1;
}
.ccm-19__cell label:hover {
background: oklch(0.36 0.035 65);
}
.ccm-19__cell :checked + label {
background: linear-gradient(150deg,var(--brand),oklch(0.68 0.16 60));
color: oklch(0.25 0.04 60);
}
.ccm-19__cell label::after {
content: "";
position: absolute;
inset: 5px;
clip-path: var(--hex);
background: transparent;
border: 0 solid transparent;
pointer-events: none;
}
.ccm-19__cell:has(:focus-visible) label::after {
background: oklch(1 0 0 / .0);
box-shadow: inset 0 0 0 3px #fff;
}
.ccm-19__cell:has(:focus-visible) label {
filter: brightness(1.15);
}
.ccm-19__note {
position: absolute;
bottom: 20px;
font-size: 12px;
letter-spacing: .08em;
text-transform: uppercase;
color: oklch(0.6 0.05 70 / .6);
}
@media (prefers-reduced-motion: reduce) {
.ccm-19 * {
transition-duration: .01s !important;
}
}.ccm-19,
.ccm-19 *,
.ccm-19 *::before,
.ccm-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-19 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
--brand: oklch(0.78 0.15 80);
--hex: polygon(25% 3.35%,75% 3.35%,100% 50%,75% 96.65%,25% 96.65%,0% 50%);
--cell: 104px;
font-family: 'Segoe UI',system-ui,sans-serif;
color: oklch(0.9 0.01 80);
}
.ccm-19__stage {
width: 100%;
height: 100vh;
position: relative;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
overflow: hidden;
}
.ccm-19__comb {
position: relative;
border: none;
width: calc(var(--cell) * 3.1);
height: calc(var(--cell) * 3.1);
display: grid;
place-items: center;
}
.ccm-19__sr {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.ccm-19__center,
.ccm-19__cell {
position: absolute;
left: 50%;
top: 50%;
width: var(--cell);
height: calc(var(--cell) * 1.1547);
}
.ccm-19__center {
translate: -50% -50%;
display: grid;
}
.ccm-19__hexface {
display: grid;
place-content: center;
justify-items: center;
gap: 2px;
clip-path: var(--hex);
background: radial-gradient(circle at 40% 30%,oklch(0.34 0.03 65),oklch(0.24 0.02 55));
text-align: center;
}
.ccm-19__hexface em {
font-style: normal;
font-size: 9.5px;
letter-spacing: .3em;
color: oklch(0.62 0.06 75);
}
.ccm-19__out {
font-size: 16.5px;
font-weight: 800;
color: var(--brand);
}
.ccm-19__cell {
--a: calc(60deg * var(--i) - 90deg);
--d: calc(var(--cell) * 1.08);
translate: calc(cos(var(--a)) * var(--d) - 50%) calc(sin(var(--a)) * var(--d) - 50%);
transition: scale .25s cubic-bezier(.34,1.35,.4,1),filter .25s;
/* second ring: 12 cells alternating --d of 2.16 and 1.87 × cell */
}
.ccm-19__cell:has(:checked) {
scale: 1.08;
z-index: 1;
filter: drop-shadow(0 8px 18px oklch(0.7 0.15 80 / .45));
}
.ccm-19__cell input {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}
.ccm-19__cell label {
position: absolute;
inset: 0;
clip-path: var(--hex);
display: grid;
place-content: center;
justify-items: center;
gap: 5px;
background: oklch(0.3 0.025 60);
border: none;
cursor: pointer;
font-size: 12.5px;
font-weight: 700;
letter-spacing: .03em;
transition: background .25s,color .25s;
}
.ccm-19__cell label b {
font-size: 21px;
font-weight: 400;
line-height: 1;
}
.ccm-19__cell label:hover {
background: oklch(0.36 0.035 65);
}
.ccm-19__cell :checked + label {
background: linear-gradient(150deg,var(--brand),oklch(0.68 0.16 60));
color: oklch(0.25 0.04 60);
}
.ccm-19__cell label::after {
content: "";
position: absolute;
inset: 5px;
clip-path: var(--hex);
background: transparent;
border: 0 solid transparent;
pointer-events: none;
}
.ccm-19__cell:has(:focus-visible) label::after {
background: oklch(1 0 0 / .0);
box-shadow: inset 0 0 0 3px #fff;
}
.ccm-19__cell:has(:focus-visible) label {
filter: brightness(1.15);
}
.ccm-19__note {
position: absolute;
bottom: 20px;
font-size: 12px;
letter-spacing: .08em;
text-transform: uppercase;
color: oklch(0.6 0.05 70 / .6);
}
@media (prefers-reduced-motion: reduce) {
.ccm-19 * {
transition-duration: .01s !important;
}
}(() => {
// Optional 3-line enhancement: mirror the picked class into the center hex.
const root = document.querySelector('.ccm-19');
const out = root.querySelector('.ccm-19__out');
root.addEventListener('change', (e) => { if (e.target.name === 'ccm19') out.textContent = e.target.value; });
})();(() => {
// Optional 3-line enhancement: mirror the picked class into the center hex.
const root = document.querySelector('.ccm-19');
const out = root.querySelector('.ccm-19__out');
root.addEventListener('change', (e) => { if (e.target.name === 'ccm19') out.textContent = e.target.value; });
})();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: Hexagonal Grid Radial Selector
Source: https://codefronts.com/navigation/css-circular-menus/hexagonal-honeycomb/
A honeycomb selector: six hexagonal cells lock around a central hex like real comb, each cut with one clip-path and placed with the same ring trig as every circle menu — hexagons ARE a radial layout with a 60° step. Radio-group semantics underneath; the picked cell fills with the accent and pops forward. No JavaScript.
## HTML
```html
<section class="ccm-19" aria-label="Hexagonal grid selector demo">
<div class="ccm-19__stage">
<fieldset class="ccm-19__comb" style="--n:6"><legend class="ccm-19__sr">Choose a class</legend>
<div class="ccm-19__center"><span class="ccm-19__hexface"><em>PICK</em><output class="ccm-19__out">Ranger</output></span></div>
<div class="ccm-19__cell" style="--i:0"><input type="radio" name="ccm19" id="ccm19-a" value="Ranger" checked><label for="ccm19-a"><b>🏹</b>Ranger</label></div>
<div class="ccm-19__cell" style="--i:1"><input type="radio" name="ccm19" id="ccm19-b" value="Mage"><label for="ccm19-b"><b>✦</b>Mage</label></div>
<div class="ccm-19__cell" style="--i:2"><input type="radio" name="ccm19" id="ccm19-c" value="Knight"><label for="ccm19-c"><b>🛡</b>Knight</label></div>
<div class="ccm-19__cell" style="--i:3"><input type="radio" name="ccm19" id="ccm19-d" value="Rogue"><label for="ccm19-d"><b>🗡</b>Rogue</label></div>
<div class="ccm-19__cell" style="--i:4"><input type="radio" name="ccm19" id="ccm19-e" value="Cleric"><label for="ccm19-e"><b>☥</b>Cleric</label></div>
<div class="ccm-19__cell" style="--i:5"><input type="radio" name="ccm19" id="ccm19-f" value="Bard"><label for="ccm19-f"><b>♪</b>Bard</label></div>
</fieldset>
<p class="ccm-19__note">Pure CSS · arrow keys cycle the comb</p>
</div>
</section>
```
## CSS
```css
.ccm-19,
.ccm-19 *,
.ccm-19 *::before,
.ccm-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-19 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
--brand: oklch(0.78 0.15 80);
--hex: polygon(25% 3.35%,75% 3.35%,100% 50%,75% 96.65%,25% 96.65%,0% 50%);
--cell: 104px;
font-family: 'Segoe UI',system-ui,sans-serif;
color: oklch(0.9 0.01 80);
}
.ccm-19__stage {
width: 100%;
height: 100vh;
position: relative;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
overflow: hidden;
}
.ccm-19__comb {
position: relative;
border: none;
width: calc(var(--cell) * 3.1);
height: calc(var(--cell) * 3.1);
display: grid;
place-items: center;
}
.ccm-19__sr {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.ccm-19__center,
.ccm-19__cell {
position: absolute;
left: 50%;
top: 50%;
width: var(--cell);
height: calc(var(--cell) * 1.1547);
}
.ccm-19__center {
translate: -50% -50%;
display: grid;
}
.ccm-19__hexface {
display: grid;
place-content: center;
justify-items: center;
gap: 2px;
clip-path: var(--hex);
background: radial-gradient(circle at 40% 30%,oklch(0.34 0.03 65),oklch(0.24 0.02 55));
text-align: center;
}
.ccm-19__hexface em {
font-style: normal;
font-size: 9.5px;
letter-spacing: .3em;
color: oklch(0.62 0.06 75);
}
.ccm-19__out {
font-size: 16.5px;
font-weight: 800;
color: var(--brand);
}
.ccm-19__cell {
--a: calc(60deg * var(--i) - 90deg);
--d: calc(var(--cell) * 1.08);
translate: calc(cos(var(--a)) * var(--d) - 50%) calc(sin(var(--a)) * var(--d) - 50%);
transition: scale .25s cubic-bezier(.34,1.35,.4,1),filter .25s;
/* second ring: 12 cells alternating --d of 2.16 and 1.87 × cell */
}
.ccm-19__cell:has(:checked) {
scale: 1.08;
z-index: 1;
filter: drop-shadow(0 8px 18px oklch(0.7 0.15 80 / .45));
}
.ccm-19__cell input {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}
.ccm-19__cell label {
position: absolute;
inset: 0;
clip-path: var(--hex);
display: grid;
place-content: center;
justify-items: center;
gap: 5px;
background: oklch(0.3 0.025 60);
border: none;
cursor: pointer;
font-size: 12.5px;
font-weight: 700;
letter-spacing: .03em;
transition: background .25s,color .25s;
}
.ccm-19__cell label b {
font-size: 21px;
font-weight: 400;
line-height: 1;
}
.ccm-19__cell label:hover {
background: oklch(0.36 0.035 65);
}
.ccm-19__cell :checked + label {
background: linear-gradient(150deg,var(--brand),oklch(0.68 0.16 60));
color: oklch(0.25 0.04 60);
}
.ccm-19__cell label::after {
content: "";
position: absolute;
inset: 5px;
clip-path: var(--hex);
background: transparent;
border: 0 solid transparent;
pointer-events: none;
}
.ccm-19__cell:has(:focus-visible) label::after {
background: oklch(1 0 0 / .0);
box-shadow: inset 0 0 0 3px #fff;
}
.ccm-19__cell:has(:focus-visible) label {
filter: brightness(1.15);
}
.ccm-19__note {
position: absolute;
bottom: 20px;
font-size: 12px;
letter-spacing: .08em;
text-transform: uppercase;
color: oklch(0.6 0.05 70 / .6);
}
@media (prefers-reduced-motion: reduce) {
.ccm-19 * {
transition-duration: .01s !important;
}
}
```
## JavaScript
```js
(() => {
// Optional 3-line enhancement: mirror the picked class into the center hex.
const root = document.querySelector('.ccm-19');
const out = root.querySelector('.ccm-19__out');
root.addEventListener('change', (e) => { if (e.target.name === 'ccm19') out.textContent = e.target.value; });
})();
```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: Hexagonal Grid Radial Selector
Source: https://codefronts.com/navigation/css-circular-menus/hexagonal-honeycomb/
A honeycomb selector: six hexagonal cells lock around a central hex like real comb, each cut with one clip-path and placed with the same ring trig as every circle menu — hexagons ARE a radial layout with a 60° step. Radio-group semantics underneath; the picked cell fills with the accent and pops forward. No JavaScript.
## HTML
```html
<section class="ccm-19" aria-label="Hexagonal grid selector demo">
<div class="ccm-19__stage">
<fieldset class="ccm-19__comb" style="--n:6"><legend class="ccm-19__sr">Choose a class</legend>
<div class="ccm-19__center"><span class="ccm-19__hexface"><em>PICK</em><output class="ccm-19__out">Ranger</output></span></div>
<div class="ccm-19__cell" style="--i:0"><input type="radio" name="ccm19" id="ccm19-a" value="Ranger" checked><label for="ccm19-a"><b>🏹</b>Ranger</label></div>
<div class="ccm-19__cell" style="--i:1"><input type="radio" name="ccm19" id="ccm19-b" value="Mage"><label for="ccm19-b"><b>✦</b>Mage</label></div>
<div class="ccm-19__cell" style="--i:2"><input type="radio" name="ccm19" id="ccm19-c" value="Knight"><label for="ccm19-c"><b>🛡</b>Knight</label></div>
<div class="ccm-19__cell" style="--i:3"><input type="radio" name="ccm19" id="ccm19-d" value="Rogue"><label for="ccm19-d"><b>🗡</b>Rogue</label></div>
<div class="ccm-19__cell" style="--i:4"><input type="radio" name="ccm19" id="ccm19-e" value="Cleric"><label for="ccm19-e"><b>☥</b>Cleric</label></div>
<div class="ccm-19__cell" style="--i:5"><input type="radio" name="ccm19" id="ccm19-f" value="Bard"><label for="ccm19-f"><b>♪</b>Bard</label></div>
</fieldset>
<p class="ccm-19__note">Pure CSS · arrow keys cycle the comb</p>
</div>
</section>
```
## CSS
```css
.ccm-19,
.ccm-19 *,
.ccm-19 *::before,
.ccm-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccm-19 {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
--brand: oklch(0.78 0.15 80);
--hex: polygon(25% 3.35%,75% 3.35%,100% 50%,75% 96.65%,25% 96.65%,0% 50%);
--cell: 104px;
font-family: 'Segoe UI',system-ui,sans-serif;
color: oklch(0.9 0.01 80);
}
.ccm-19__stage {
width: 100%;
height: 100vh;
position: relative;
display: grid;
place-items: center;
border-radius: 20px;
background: linear-gradient(180deg,oklch(0.25 0.02 60),oklch(0.18 0.018 50));
overflow: hidden;
}
.ccm-19__comb {
position: relative;
border: none;
width: calc(var(--cell) * 3.1);
height: calc(var(--cell) * 3.1);
display: grid;
place-items: center;
}
.ccm-19__sr {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
.ccm-19__center,
.ccm-19__cell {
position: absolute;
left: 50%;
top: 50%;
width: var(--cell);
height: calc(var(--cell) * 1.1547);
}
.ccm-19__center {
translate: -50% -50%;
display: grid;
}
.ccm-19__hexface {
display: grid;
place-content: center;
justify-items: center;
gap: 2px;
clip-path: var(--hex);
background: radial-gradient(circle at 40% 30%,oklch(0.34 0.03 65),oklch(0.24 0.02 55));
text-align: center;
}
.ccm-19__hexface em {
font-style: normal;
font-size: 9.5px;
letter-spacing: .3em;
color: oklch(0.62 0.06 75);
}
.ccm-19__out {
font-size: 16.5px;
font-weight: 800;
color: var(--brand);
}
.ccm-19__cell {
--a: calc(60deg * var(--i) - 90deg);
--d: calc(var(--cell) * 1.08);
translate: calc(cos(var(--a)) * var(--d) - 50%) calc(sin(var(--a)) * var(--d) - 50%);
transition: scale .25s cubic-bezier(.34,1.35,.4,1),filter .25s;
/* second ring: 12 cells alternating --d of 2.16 and 1.87 × cell */
}
.ccm-19__cell:has(:checked) {
scale: 1.08;
z-index: 1;
filter: drop-shadow(0 8px 18px oklch(0.7 0.15 80 / .45));
}
.ccm-19__cell input {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}
.ccm-19__cell label {
position: absolute;
inset: 0;
clip-path: var(--hex);
display: grid;
place-content: center;
justify-items: center;
gap: 5px;
background: oklch(0.3 0.025 60);
border: none;
cursor: pointer;
font-size: 12.5px;
font-weight: 700;
letter-spacing: .03em;
transition: background .25s,color .25s;
}
.ccm-19__cell label b {
font-size: 21px;
font-weight: 400;
line-height: 1;
}
.ccm-19__cell label:hover {
background: oklch(0.36 0.035 65);
}
.ccm-19__cell :checked + label {
background: linear-gradient(150deg,var(--brand),oklch(0.68 0.16 60));
color: oklch(0.25 0.04 60);
}
.ccm-19__cell label::after {
content: "";
position: absolute;
inset: 5px;
clip-path: var(--hex);
background: transparent;
border: 0 solid transparent;
pointer-events: none;
}
.ccm-19__cell:has(:focus-visible) label::after {
background: oklch(1 0 0 / .0);
box-shadow: inset 0 0 0 3px #fff;
}
.ccm-19__cell:has(:focus-visible) label {
filter: brightness(1.15);
}
.ccm-19__note {
position: absolute;
bottom: 20px;
font-size: 12px;
letter-spacing: .08em;
text-transform: uppercase;
color: oklch(0.6 0.05 70 / .6);
}
@media (prefers-reduced-motion: reduce) {
.ccm-19 * {
transition-duration: .01s !important;
}
}
```
## JavaScript
```js
(() => {
// Optional 3-line enhancement: mirror the picked class into the center hex.
const root = document.querySelector('.ccm-19');
const out = root.querySelector('.ccm-19__out');
root.addEventListener('change', (e) => { if (e.target.name === 'ccm19') out.textContent = e.target.value; });
})();
```How this works
Every cell shares one shape:
clip-path: polygon(25% 3.35%, 75% 3.35%,
100% 50%, 75% 96.65%, 25% 96.65%, 0% 50%);For pointy-side tiling, neighbors sit at rotate steps of 60° starting at -60°, at a center distance of exactly cell-width × 0.995 here (√3/2 × height ratios fold into the width/height of the cell box). That's the whole honeycomb 'grid system': one clip shape + the standard polar placement loop.
Selection is :checked on visually-hidden radios — arrow keys cycle cells natively. Since outline can't follow a clip-path, the focus ring is drawn as an inner hex: a ::after pseudo with the same clip, inset by 5px, shown when :has(:focus-visible).
Make it yours
- Cell size: one
--celltoken; the spacing formula scales with it. - Honeycomb gap: the
--gapmultiplier in the translate distance. - Second ring: 12 more cells at alternating distances — the comment in the CSS shows the two radii.
- Flat-top hexes: swap the polygon and add 30° to every angle.
Gotchas — read before shipping
- clip-path removes the border box visually but NOT the hit area outside the clip — overlapping hexes must not overlap their neighbors' clickable rectangles; the tight translate distance here is deliberate.
- Standard outlines ignore clip-path — draw focus indicators inside the shape (inner hex or brightness shift).
- Hex labels need generous padding; corners eat into text fast at small sizes.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 121+ | 111+ |
Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 105+.
clip-path polygons are ancient; :has() for the checked/focus styling is the 2023 baseline. Trig placement as elsewhere.