25 CSS Button Groups
01 / 25

All 25 in 25 CSS Button Groups

  1. 01CSS Split Button with Anchor-Positioned Popover Menu (Popover API, No JS)
  2. 02CSS Segmented Control with Motion-Path Comet Thumb (offset-path Arc Travel)
  3. 03CSS View Transitions Toggle Button Group with Morphing Thumb (Shared-Element FLIP)
  4. 04CSS Pricing Toggle with Monthly / Yearly Switch (Pure CSS, No JavaScript)
  5. 05CSS Segmented Control with Sliding Thumb Indicator (iOS-Style, Pure CSS)
  6. 06CSS Split Button Dropdown with Native <details> (Primary Action + Menu)
  7. 07CSS Icon Button Group for Toolbar Actions (Accessible Tooltips, Pure CSS)
  8. 08CSS Pill Radio Button Group with Sliding Indicator (Filter Pills, Pure CSS)
  9. 09CSS Toggle Button Group with Radio Inputs (Single-Select Alignment Group)
  10. 10CSS Wallet Connect Button Grid for Web3 Sign-In (MetaMask, WalletConnect, Coinbase)
  11. 11CSS AI Model Selector Button Group with Provider Badges (LLM Model Picker)
  12. 12CSS Segmented Control with Liquid Mercury Thumb (Metallic Stretch Animation)
  13. 13CSS Button Group with Subgrid Underline Beam (Perfectly Aligned Tab Indicator)
  14. 14CSS Multi-Select Chip Buttons with Acid Glow (Toggleable Tag Filters)
  15. 15CSS Split Button with Quantum Chevron Reveal (Animated Menu Materialize)
  16. 16CSS Format Toggle Button Group with Conic Fill (Radial Sweep Select)
  17. 17CSS Pixel Matrix Button Group with LED Segment Glow (Retro Display Buttons)
  18. 18CSS Multi-Action Button Group with Gradient Flow Progress (Animated CTA Bar)
  19. 19CSS Icon Segmented Control with Holographic Sheen (Iridescent View Switcher)
  20. 20CSS Vertical Button Stack with 3D Hover Depth (Tilting Card Buttons)
  21. 21CSS Floating Action Button with Radial Sub-Button Menu (Arc Fan-Out, Pure CSS)
  22. 22CSS Filter Button Group with RGB-Split Glitch Active State (Cyberpunk Toggle)
  23. 23CSS Pill Button Group with Rotating Conic Plasma Border (Animated Gradient Ring)
  24. 24CSS Action Button Row with Radar Shockwave Click Feedback (Ripple Pulse)
  25. 25CSS Date-Range Preset Button Group with Cursor-Following Glow (Spotlight Hover)
Pure CSSMIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS

ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

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>
.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;
  }
}
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 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-area value (bottom / top / inline-end etc.).
  • Auto-flip behavior = position-try-fallbacks.
  • Primary color = --accent; the caret derives a darker shade.
  • Entrance feel = the @starting-style offset + transition duration.

Gotchas — read before shipping

  • Anchor positioning + allow-discrete are 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=menu if you need the full pattern.
  • Keep the primary action a separate <button> so it never opens the menu.
  • Always set a position-try-fallbacks or the menu can render off-screen near a viewport edge.

Browser support

ChromeSafariFirefoxEdge
125+26+flag125+

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.

Techniques used in this demo

Search CodeFronts

Loading…