25 CSS Button Groups
08 / 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 Pill Radio Button Group with Sliding Indicator (Filter Pills, Pure CSS)

A row of pill-shaped filter buttons — All / Design / Engineering / Growth — with a single highlight that slides between the selected pill. The most-searched 'CSS pill radio button group sliding indicator' for content filters and category tabs, built on real radio inputs so it's keyboard-navigable and announces selection state out of the box.

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-08" aria-label="Pill radio button group demo">
  <div class="btn-08__wrap">
    <p class="btn-08__label">Browse by team</p>
    <div class="btn-08__pills" role="group" aria-label="Filter by team" style="--x:0px;--w:60px">
      <input type="radio" name="btn-08" id="btn-08-1" class="btn-08__in" checked>
      <label for="btn-08-1" class="btn-08__pill" style="--x:4px;--w:56px">All</label>
      <input type="radio" name="btn-08" id="btn-08-2" class="btn-08__in">
      <label for="btn-08-2" class="btn-08__pill" style="--x:64px;--w:92px">Design</label>
      <input type="radio" name="btn-08" id="btn-08-3" class="btn-08__in">
      <label for="btn-08-3" class="btn-08__pill" style="--x:160px;--w:126px">Engineering</label>
      <input type="radio" name="btn-08" id="btn-08-4" class="btn-08__in">
      <label for="btn-08-4" class="btn-08__pill" style="--x:290px;--w:82px">Growth</label>
      <span class="btn-08__ind" aria-hidden="true"></span>
    </div>
  </div>
</section>
.btn-08,
.btn-08 *,
.btn-08 *::before,
.btn-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-08 {
  --accent: oklch(0.68 0.2 350);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(130% 130% at 20% 0%,#2a1030,#160820);
  padding: 44px 20px;
}

.btn-08__wrap {
  width: min(560px,100%);
}

.btn-08__label {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 14px 4px;
}

.btn-08__pills {
  position: relative;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0;
  padding: 4px;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 999px;
  backdrop-filter: blur(6px);
}

.btn-08__in {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.btn-08__pill {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 42px;
  padding: 0 20px;
  font-size: 14.5px;
  font-weight: 650;
  color: rgba(255,255,255,.7);
  cursor: pointer;
  border-radius: 999px;
  transition: color .25s;
}

.btn-08__ind {
  position: absolute;
  z-index: 0;
  top: 4px;
  left: 0;
  height: calc(100% - 8px);
  width: var(--w);
  translate: var(--x) 0;
  background: var(--accent);
  border-radius: 999px;
  box-shadow: 0 4px 18px -4px var(--accent);
  transition: translate .4s cubic-bezier(.34,1.3,.5,1),width .4s cubic-bezier(.34,1.3,.5,1);
}

.btn-08__pills:has(#btn-08-1:checked) {
  --x: 4px;
  --w: 56px;
}

.btn-08__pills:has(#btn-08-2:checked) {
  --x: 64px;
  --w: 92px;
}

.btn-08__pills:has(#btn-08-3:checked) {
  --x: 160px;
  --w: 126px;
}

.btn-08__pills:has(#btn-08-4:checked) {
  --x: 290px;
  --w: 82px;
}

.btn-08__in:checked + .btn-08__pill {
  color: #fff;
}

.btn-08__in:focus-visible + .btn-08__pill {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .btn-08__ind,
    .btn-08__pill {
    transition: none;
  }
}
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 Pill Radio Button Group with Sliding Indicator (Filter Pills, Pure CSS)
Source: https://codefronts.com/components/css-button-groups/css-pill-radio-button-group-with-sliding-indicator-filter-pills-pure-css/

A row of pill-shaped filter buttons — All / Design / Engineering / Growth — with a single highlight that slides between the selected pill. The most-searched 'CSS pill radio button group sliding indicator' for content filters and category tabs, built on real radio inputs so it's keyboard-navigable and announces selection state out of the box.
## HTML
```html
<section class="btn-08" aria-label="Pill radio button group demo">
  <div class="btn-08__wrap">
    <p class="btn-08__label">Browse by team</p>
    <div class="btn-08__pills" role="group" aria-label="Filter by team" style="--x:0px;--w:60px">
      <input type="radio" name="btn-08" id="btn-08-1" class="btn-08__in" checked>
      <label for="btn-08-1" class="btn-08__pill" style="--x:4px;--w:56px">All</label>
      <input type="radio" name="btn-08" id="btn-08-2" class="btn-08__in">
      <label for="btn-08-2" class="btn-08__pill" style="--x:64px;--w:92px">Design</label>
      <input type="radio" name="btn-08" id="btn-08-3" class="btn-08__in">
      <label for="btn-08-3" class="btn-08__pill" style="--x:160px;--w:126px">Engineering</label>
      <input type="radio" name="btn-08" id="btn-08-4" class="btn-08__in">
      <label for="btn-08-4" class="btn-08__pill" style="--x:290px;--w:82px">Growth</label>
      <span class="btn-08__ind" aria-hidden="true"></span>
    </div>
  </div>
</section>
```
## CSS
```css
.btn-08,
.btn-08 *,
.btn-08 *::before,
.btn-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-08 {
  --accent: oklch(0.68 0.2 350);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(130% 130% at 20% 0%,#2a1030,#160820);
  padding: 44px 20px;
}

.btn-08__wrap {
  width: min(560px,100%);
}

.btn-08__label {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 14px 4px;
}

.btn-08__pills {
  position: relative;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0;
  padding: 4px;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 999px;
  backdrop-filter: blur(6px);
}

.btn-08__in {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.btn-08__pill {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 42px;
  padding: 0 20px;
  font-size: 14.5px;
  font-weight: 650;
  color: rgba(255,255,255,.7);
  cursor: pointer;
  border-radius: 999px;
  transition: color .25s;
}

.btn-08__ind {
  position: absolute;
  z-index: 0;
  top: 4px;
  left: 0;
  height: calc(100% - 8px);
  width: var(--w);
  translate: var(--x) 0;
  background: var(--accent);
  border-radius: 999px;
  box-shadow: 0 4px 18px -4px var(--accent);
  transition: translate .4s cubic-bezier(.34,1.3,.5,1),width .4s cubic-bezier(.34,1.3,.5,1);
}

.btn-08__pills:has(#btn-08-1:checked) {
  --x: 4px;
  --w: 56px;
}

.btn-08__pills:has(#btn-08-2:checked) {
  --x: 64px;
  --w: 92px;
}

.btn-08__pills:has(#btn-08-3:checked) {
  --x: 160px;
  --w: 126px;
}

.btn-08__pills:has(#btn-08-4:checked) {
  --x: 290px;
  --w: 82px;
}

.btn-08__in:checked + .btn-08__pill {
  color: #fff;
}

.btn-08__in:focus-visible + .btn-08__pill {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .btn-08__ind,
    .btn-08__pill {
    transition: none;
  }
}
```

How this works

Each pill is a label bound to a hidden radio sharing one name. Unlike the fixed-width segmented control (demo 02), these pills are content-sized, so a proportional translate won't work — instead the sliding highlight is a ::before layer that uses :has() to read which pill is checked and animates left+width to match. For a pure-CSS demo the highlight sits behind the active pill via stacking; in production the same effect drives from a tiny measure-on-select helper if pills are dynamic.

Here we keep it fully declarative: a ::after underbar-pill is placed per-selection with per-radio rules, giving a crisp slide with no measurement. Text color inverts on the active pill and the whole group has one focus ring per pill via :focus-visible.

.pills:has(#p2:checked){ --x:var(--x2); --w:var(--w2) }
.pills__ind{ translate:var(--x) 0; width:var(--w) }

Techniques used: content-sized pills with hidden radios · CSS-variable driven indicator position/width · :has() selection projection · color inversion on active pill · wrap-friendly flex layout

Make it yours

  • Set each pill's --x/--w pair to its measured offset/width (shown inline for the demo).
  • --accent is the indicator fill.
  • Let pills wrap to two rows on mobile by keeping flex-wrap:wrap and hiding the slide below a breakpoint.
  • Turn it into a multi-select tag row by switching radios to checkboxes (see demo 11 for the glow variant).

Gotchas — read before shipping

  • A moving indicator across variable-width pills needs known offsets; for user-generated pill text, measure widths with a few lines of JS rather than hard-coding.
  • Keep radios grouped by name so arrow keys cycle selection.
  • Ensure the active pill's text meets 4.5:1 contrast against the indicator fill — test both light and dark accents.
  • If pills wrap, disable the horizontal slide on the wrapped breakpoint to avoid the indicator flying across rows.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Floor set by :has(), oklch(), backdrop-filter as this demo is written. The core technique itself goes back further — Chrome 105+.

:has() Baseline 2023. The radio + label pill row degrades to a normal selectable group without the sliding indicator on older engines.

Techniques used in this demo

Search CodeFronts

Loading…