25 CSS Button Groups01 / 25
CSS Split Button with Anchor-Positioned Popover Menu (Popover API, No JS)
A split button whose dropdown is tethered with pure CSS Anchor Positioning and opened by the native Popover API — the menu docks perfectly under the caret, escapes every overflow/clipping ancestor via the top layer, and needs zero JavaScript. The cutting-edge 'CSS anchor positioning dropdown button with Popover API' pattern front-end devs are adopting in 2027.
Published
The code
<section class="btn-01" aria-label="Anchor-positioned popover split button demo">
<div class="btn-01__split">
<button type="button" class="btn-01__primary">Export CSV</button>
<button type="button" class="btn-01__caret" popovertarget="btn-01-menu" aria-label="More export formats"></button>
<div id="btn-01-menu" popover class="btn-01__menu" role="menu">
<button type="button" class="btn-01__item" role="menuitem">Export as JSON</button>
<button type="button" class="btn-01__item" role="menuitem">Export as Excel</button>
<button type="button" class="btn-01__item" role="menuitem">Export as PDF</button>
<div class="btn-01__sep"></div>
<button type="button" class="btn-01__item" role="menuitem">Copy to clipboard</button>
</div>
</div>
</section><section class="btn-01" aria-label="Anchor-positioned popover split button demo">
<div class="btn-01__split">
<button type="button" class="btn-01__primary">Export CSV</button>
<button type="button" class="btn-01__caret" popovertarget="btn-01-menu" aria-label="More export formats"></button>
<div id="btn-01-menu" popover class="btn-01__menu" role="menu">
<button type="button" class="btn-01__item" role="menuitem">Export as JSON</button>
<button type="button" class="btn-01__item" role="menuitem">Export as Excel</button>
<button type="button" class="btn-01__item" role="menuitem">Export as PDF</button>
<div class="btn-01__sep"></div>
<button type="button" class="btn-01__item" role="menuitem">Copy to clipboard</button>
</div>
</div>
</section>.btn-01,
.btn-01 *,
.btn-01 *::before,
.btn-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-01 {
--accent: oklch(0.63 0.21 30);
--accent-d: oklch(0.54 0.2 30);
--ink: #fff;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
background: linear-gradient(160deg,#eef0f4,#e0e3ea);
padding: 120px 20px;
}
.btn-01__split {
display: inline-flex;
border-radius: 14px;
box-shadow: 0 14px 34px -16px rgba(120,30,10,.5);
}
.btn-01__primary {
border: none;
background: var(--accent);
color: var(--ink);
font-size: 15.5px;
font-weight: 750;
padding: 0 22px;
height: 50px;
border-radius: 14px 0 0 14px;
cursor: pointer;
transition: filter .15s;
}
.btn-01__primary:hover {
filter: brightness(1.06);
}
.btn-01__caret {
anchor-name: --btn-01-anc;
position: relative;
border: none;
width: 48px;
height: 50px;
background: var(--accent-d);
border-radius: 0 14px 14px 0;
cursor: pointer;
border-left: 1px solid rgba(90,20,5,.3);
}
.btn-01__caret::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 9px;
height: 9px;
border-right: 2.5px solid var(--ink);
border-bottom: 2.5px solid var(--ink);
translate: -50% -70%;
rotate: 45deg;
transition: rotate .25s,translate .25s;
}
.btn-01__caret:hover {
filter: brightness(1.08);
}
.btn-01__menu {
position: fixed;
margin: 0;
inset: auto;
position-anchor: --btn-01-anc;
top: anchor(bottom);
right: anchor(right);
margin-top: 8px;
position-try-fallbacks: flip-block;
min-width: 220px;
background: #fff;
border: 1px solid #e6dcd8;
border-radius: 14px;
padding: 7px;
box-shadow: 0 26px 54px -22px rgba(120,40,20,.5);
display: flex;
flex-direction: column;
gap: 2px;
opacity: 0;
translate: 0 -6px;
transition: opacity .2s,translate .2s,display .2s allow-discrete,overlay .2s allow-discrete;
}
.btn-01__menu:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
.btn-01__menu:popover-open {
opacity: 0;
translate: 0 -6px;
}
}
.btn-01__item {
text-align: left;
border: none;
background: none;
font-size: 14.5px;
font-weight: 650;
color: #4a2a20;
padding: 11px 13px;
border-radius: 9px;
cursor: pointer;
transition: background .12s;
}
.btn-01__item:hover {
background: #fbeeea;
}
.btn-01__sep {
height: 1px;
background: #f0e2dd;
margin: 5px 6px;
}
.btn-01__primary:focus-visible,
.btn-01__caret:focus-visible,
.btn-01__item:focus-visible {
outline: 3px solid var(--accent-d);
outline-offset: 2px;
}
@supports not (anchor-name: --x) {
.btn-01__menu {
top: auto;
bottom: 16px;
right: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.btn-01__menu,
.btn-01__caret::after {
transition: opacity .12s,display .12s allow-discrete,overlay .12s allow-discrete;
}
}.btn-01,
.btn-01 *,
.btn-01 *::before,
.btn-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-01 {
--accent: oklch(0.63 0.21 30);
--accent-d: oklch(0.54 0.2 30);
--ink: #fff;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
background: linear-gradient(160deg,#eef0f4,#e0e3ea);
padding: 120px 20px;
}
.btn-01__split {
display: inline-flex;
border-radius: 14px;
box-shadow: 0 14px 34px -16px rgba(120,30,10,.5);
}
.btn-01__primary {
border: none;
background: var(--accent);
color: var(--ink);
font-size: 15.5px;
font-weight: 750;
padding: 0 22px;
height: 50px;
border-radius: 14px 0 0 14px;
cursor: pointer;
transition: filter .15s;
}
.btn-01__primary:hover {
filter: brightness(1.06);
}
.btn-01__caret {
anchor-name: --btn-01-anc;
position: relative;
border: none;
width: 48px;
height: 50px;
background: var(--accent-d);
border-radius: 0 14px 14px 0;
cursor: pointer;
border-left: 1px solid rgba(90,20,5,.3);
}
.btn-01__caret::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 9px;
height: 9px;
border-right: 2.5px solid var(--ink);
border-bottom: 2.5px solid var(--ink);
translate: -50% -70%;
rotate: 45deg;
transition: rotate .25s,translate .25s;
}
.btn-01__caret:hover {
filter: brightness(1.08);
}
.btn-01__menu {
position: fixed;
margin: 0;
inset: auto;
position-anchor: --btn-01-anc;
top: anchor(bottom);
right: anchor(right);
margin-top: 8px;
position-try-fallbacks: flip-block;
min-width: 220px;
background: #fff;
border: 1px solid #e6dcd8;
border-radius: 14px;
padding: 7px;
box-shadow: 0 26px 54px -22px rgba(120,40,20,.5);
display: flex;
flex-direction: column;
gap: 2px;
opacity: 0;
translate: 0 -6px;
transition: opacity .2s,translate .2s,display .2s allow-discrete,overlay .2s allow-discrete;
}
.btn-01__menu:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
.btn-01__menu:popover-open {
opacity: 0;
translate: 0 -6px;
}
}
.btn-01__item {
text-align: left;
border: none;
background: none;
font-size: 14.5px;
font-weight: 650;
color: #4a2a20;
padding: 11px 13px;
border-radius: 9px;
cursor: pointer;
transition: background .12s;
}
.btn-01__item:hover {
background: #fbeeea;
}
.btn-01__sep {
height: 1px;
background: #f0e2dd;
margin: 5px 6px;
}
.btn-01__primary:focus-visible,
.btn-01__caret:focus-visible,
.btn-01__item:focus-visible {
outline: 3px solid var(--accent-d);
outline-offset: 2px;
}
@supports not (anchor-name: --x) {
.btn-01__menu {
top: auto;
bottom: 16px;
right: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.btn-01__menu,
.btn-01__caret::after {
transition: opacity .12s,display .12s allow-discrete,overlay .12s allow-discrete;
}
}Here's a working CSS Button Group 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: CSS Split Button with Anchor-Positioned Popover Menu (Popover API, No JS)
Source: https://codefronts.com/components/css-button-groups/css-split-button-with-anchor-positioned-popover-menu-popover-api-no-js/
A split button whose dropdown is tethered with pure CSS Anchor Positioning and opened by the native Popover API — the menu docks perfectly under the caret, escapes every overflow/clipping ancestor via the top layer, and needs zero JavaScript. The cutting-edge 'CSS anchor positioning dropdown button with Popover API' pattern front-end devs are adopting in 2027.
## HTML
```html
<section class="btn-01" aria-label="Anchor-positioned popover split button demo">
<div class="btn-01__split">
<button type="button" class="btn-01__primary">Export CSV</button>
<button type="button" class="btn-01__caret" popovertarget="btn-01-menu" aria-label="More export formats"></button>
<div id="btn-01-menu" popover class="btn-01__menu" role="menu">
<button type="button" class="btn-01__item" role="menuitem">Export as JSON</button>
<button type="button" class="btn-01__item" role="menuitem">Export as Excel</button>
<button type="button" class="btn-01__item" role="menuitem">Export as PDF</button>
<div class="btn-01__sep"></div>
<button type="button" class="btn-01__item" role="menuitem">Copy to clipboard</button>
</div>
</div>
</section>
```
## CSS
```css
.btn-01,
.btn-01 *,
.btn-01 *::before,
.btn-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-01 {
--accent: oklch(0.63 0.21 30);
--accent-d: oklch(0.54 0.2 30);
--ink: #fff;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
background: linear-gradient(160deg,#eef0f4,#e0e3ea);
padding: 120px 20px;
}
.btn-01__split {
display: inline-flex;
border-radius: 14px;
box-shadow: 0 14px 34px -16px rgba(120,30,10,.5);
}
.btn-01__primary {
border: none;
background: var(--accent);
color: var(--ink);
font-size: 15.5px;
font-weight: 750;
padding: 0 22px;
height: 50px;
border-radius: 14px 0 0 14px;
cursor: pointer;
transition: filter .15s;
}
.btn-01__primary:hover {
filter: brightness(1.06);
}
.btn-01__caret {
anchor-name: --btn-01-anc;
position: relative;
border: none;
width: 48px;
height: 50px;
background: var(--accent-d);
border-radius: 0 14px 14px 0;
cursor: pointer;
border-left: 1px solid rgba(90,20,5,.3);
}
.btn-01__caret::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 9px;
height: 9px;
border-right: 2.5px solid var(--ink);
border-bottom: 2.5px solid var(--ink);
translate: -50% -70%;
rotate: 45deg;
transition: rotate .25s,translate .25s;
}
.btn-01__caret:hover {
filter: brightness(1.08);
}
.btn-01__menu {
position: fixed;
margin: 0;
inset: auto;
position-anchor: --btn-01-anc;
top: anchor(bottom);
right: anchor(right);
margin-top: 8px;
position-try-fallbacks: flip-block;
min-width: 220px;
background: #fff;
border: 1px solid #e6dcd8;
border-radius: 14px;
padding: 7px;
box-shadow: 0 26px 54px -22px rgba(120,40,20,.5);
display: flex;
flex-direction: column;
gap: 2px;
opacity: 0;
translate: 0 -6px;
transition: opacity .2s,translate .2s,display .2s allow-discrete,overlay .2s allow-discrete;
}
.btn-01__menu:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
.btn-01__menu:popover-open {
opacity: 0;
translate: 0 -6px;
}
}
.btn-01__item {
text-align: left;
border: none;
background: none;
font-size: 14.5px;
font-weight: 650;
color: #4a2a20;
padding: 11px 13px;
border-radius: 9px;
cursor: pointer;
transition: background .12s;
}
.btn-01__item:hover {
background: #fbeeea;
}
.btn-01__sep {
height: 1px;
background: #f0e2dd;
margin: 5px 6px;
}
.btn-01__primary:focus-visible,
.btn-01__caret:focus-visible,
.btn-01__item:focus-visible {
outline: 3px solid var(--accent-d);
outline-offset: 2px;
}
@supports not (anchor-name: --x) {
.btn-01__menu {
top: auto;
bottom: 16px;
right: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.btn-01__menu,
.btn-01__caret::after {
transition: opacity .12s,display .12s allow-discrete,overlay .12s allow-discrete;
}
}
```Here's a working CSS Button Group 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: CSS Split Button with Anchor-Positioned Popover Menu (Popover API, No JS)
Source: https://codefronts.com/components/css-button-groups/css-split-button-with-anchor-positioned-popover-menu-popover-api-no-js/
A split button whose dropdown is tethered with pure CSS Anchor Positioning and opened by the native Popover API — the menu docks perfectly under the caret, escapes every overflow/clipping ancestor via the top layer, and needs zero JavaScript. The cutting-edge 'CSS anchor positioning dropdown button with Popover API' pattern front-end devs are adopting in 2027.
## HTML
```html
<section class="btn-01" aria-label="Anchor-positioned popover split button demo">
<div class="btn-01__split">
<button type="button" class="btn-01__primary">Export CSV</button>
<button type="button" class="btn-01__caret" popovertarget="btn-01-menu" aria-label="More export formats"></button>
<div id="btn-01-menu" popover class="btn-01__menu" role="menu">
<button type="button" class="btn-01__item" role="menuitem">Export as JSON</button>
<button type="button" class="btn-01__item" role="menuitem">Export as Excel</button>
<button type="button" class="btn-01__item" role="menuitem">Export as PDF</button>
<div class="btn-01__sep"></div>
<button type="button" class="btn-01__item" role="menuitem">Copy to clipboard</button>
</div>
</div>
</section>
```
## CSS
```css
.btn-01,
.btn-01 *,
.btn-01 *::before,
.btn-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-01 {
--accent: oklch(0.63 0.21 30);
--accent-d: oklch(0.54 0.2 30);
--ink: #fff;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
background: linear-gradient(160deg,#eef0f4,#e0e3ea);
padding: 120px 20px;
}
.btn-01__split {
display: inline-flex;
border-radius: 14px;
box-shadow: 0 14px 34px -16px rgba(120,30,10,.5);
}
.btn-01__primary {
border: none;
background: var(--accent);
color: var(--ink);
font-size: 15.5px;
font-weight: 750;
padding: 0 22px;
height: 50px;
border-radius: 14px 0 0 14px;
cursor: pointer;
transition: filter .15s;
}
.btn-01__primary:hover {
filter: brightness(1.06);
}
.btn-01__caret {
anchor-name: --btn-01-anc;
position: relative;
border: none;
width: 48px;
height: 50px;
background: var(--accent-d);
border-radius: 0 14px 14px 0;
cursor: pointer;
border-left: 1px solid rgba(90,20,5,.3);
}
.btn-01__caret::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 9px;
height: 9px;
border-right: 2.5px solid var(--ink);
border-bottom: 2.5px solid var(--ink);
translate: -50% -70%;
rotate: 45deg;
transition: rotate .25s,translate .25s;
}
.btn-01__caret:hover {
filter: brightness(1.08);
}
.btn-01__menu {
position: fixed;
margin: 0;
inset: auto;
position-anchor: --btn-01-anc;
top: anchor(bottom);
right: anchor(right);
margin-top: 8px;
position-try-fallbacks: flip-block;
min-width: 220px;
background: #fff;
border: 1px solid #e6dcd8;
border-radius: 14px;
padding: 7px;
box-shadow: 0 26px 54px -22px rgba(120,40,20,.5);
display: flex;
flex-direction: column;
gap: 2px;
opacity: 0;
translate: 0 -6px;
transition: opacity .2s,translate .2s,display .2s allow-discrete,overlay .2s allow-discrete;
}
.btn-01__menu:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
.btn-01__menu:popover-open {
opacity: 0;
translate: 0 -6px;
}
}
.btn-01__item {
text-align: left;
border: none;
background: none;
font-size: 14.5px;
font-weight: 650;
color: #4a2a20;
padding: 11px 13px;
border-radius: 9px;
cursor: pointer;
transition: background .12s;
}
.btn-01__item:hover {
background: #fbeeea;
}
.btn-01__sep {
height: 1px;
background: #f0e2dd;
margin: 5px 6px;
}
.btn-01__primary:focus-visible,
.btn-01__caret:focus-visible,
.btn-01__item:focus-visible {
outline: 3px solid var(--accent-d);
outline-offset: 2px;
}
@supports not (anchor-name: --x) {
.btn-01__menu {
top: auto;
bottom: 16px;
right: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.btn-01__menu,
.btn-01__caret::after {
transition: opacity .12s,display .12s allow-discrete,overlay .12s allow-discrete;
}
}
```How this works
The caret button declares anchor-name:--btn-01-anc and points at the menu with the native popovertarget attribute; the menu is a real popover. The browser opens it in the top layer (so it never gets clipped by overflow:hidden or z-index traps) and pins it with position-anchor:--btn-01-anc + position-area:bottom span-inline-start — no offset math, no positioning library, no JS.
The entrance is declarative too: @starting-style gives the popover its from-state and transition-behavior:allow-discrete lets display/overlay animate, so the menu fades and lifts in and cleanly reverses out. position-try-fallbacks flips it above the button when there's no room below.
.btn-01__caret{ anchor-name:--btn-01-anc }
.btn-01__menu{ position-anchor:--btn-01-anc; position-area:bottom span-inline-start;
position-try-fallbacks:flip-block }
@starting-style{ .btn-01__menu:popover-open{ opacity:0; translate:0 -6px } }Techniques used: CSS Anchor Positioning (anchor-name / position-anchor / position-area) · native Popover API (popovertarget, top layer) · @starting-style entrance · transition-behavior:allow-discrete for display/overlay · position-try-fallbacks auto-flip · light-dismiss + Escape for free
Make it yours
- Dock side = the
position-areavalue (bottom / top / inline-end etc.). - Auto-flip behavior =
position-try-fallbacks. - Primary color =
--accent; the caret derives a darker shade. - Entrance feel = the
@starting-styleoffset + transition duration.
Gotchas — read before shipping
- Anchor positioning +
allow-discreteare Baseline 2024 but landed in Chrome 125/Safari 26 — provide a static absolute-positioned fallback with@supports not (anchor-name:--x). - The Popover API gives light-dismiss and Escape for free, but it's a disclosure, not a strict ARIA menu — add
role=menuif you need the full pattern. - Keep the primary action a separate
<button>so it never opens the menu. - Always set a
position-try-fallbacksor the menu can render off-screen near a viewport edge.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 125+ | 26+ | flag | 125+ |
Anchor Positioning is Baseline 2024 (Chrome/Safari shipped; Firefox behind a flag). The Popover API is Baseline 2024 everywhere. Use @supports for a graceful absolute-positioned fallback in older/Firefox.