25 CSS Button Groups
04 / 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 Pricing Toggle with Monthly / Yearly Switch (Pure CSS, No JavaScript)

The SaaS pricing-page staple: a two-option billing switch where flipping between Monthly and Yearly slides a thumb across the track AND swaps every price on the card — the exact 'CSS pricing toggle monthly yearly switch without JavaScript' pattern product teams in the US, UK, Canada and Australia ship on landing pages. Real radio inputs, keyboard-operable, screen-reader friendly.

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-04" aria-label="Monthly yearly pricing toggle demo">
  <div class="btn-04__card">
    <div class="btn-04__toggle" role="group" aria-label="Billing period">
      <div class="btn-04__track">
        <input type="radio" name="btn-04" id="btn-04-m" class="btn-04__in" checked>
        <label for="btn-04-m" class="btn-04__opt">Monthly</label>
        <input type="radio" name="btn-04" id="btn-04-y" class="btn-04__in">
        <label for="btn-04-y" class="btn-04__opt">Yearly <span class="btn-04__save">Save 20%</span></label>
        <span class="btn-04__thumb" aria-hidden="true"></span>
      </div>
    </div>
    <div class="btn-04__plans">
      <div class="btn-04__plan">
        <p class="btn-04__name">Starter</p>
        <p class="btn-04__price" aria-live="polite"><span class="btn-04__mo"><b>$12</b>/mo</span><span class="btn-04__yr"><b>$115</b>/yr</span></p>
        <p class="btn-04__note">Billed per seat, cancel anytime.</p>
      </div>
      <div class="btn-04__plan btn-04__plan--hot">
        <p class="btn-04__name">Studio</p>
        <p class="btn-04__price" aria-live="polite"><span class="btn-04__mo"><b>$29</b>/mo</span><span class="btn-04__yr"><b>$278</b>/yr</span></p>
        <p class="btn-04__note">Everything in Starter, unlimited projects.</p>
      </div>
    </div>
  </div>
</section>
.btn-04,
.btn-04 *,
.btn-04 *::before,
.btn-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-04 {
  --accent: oklch(0.66 0.15 195);
  --ink: #0d2b2e;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 120% at 50% 0%,#eafaf6,#d6efe9);
  padding: 44px 20px;
}

.btn-04__card {
  width: min(560px,100%);
  background: #fff;
  border-radius: 24px;
  padding: 32px 30px 34px;
  box-shadow: 0 30px 70px -40px rgba(6,60,58,.5);
}

.btn-04__toggle {
  display: flex;
  justify-content: center;
  margin-bottom: 26px;
}

.btn-04__track {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 5px;
  background: #e4f1ee;
  border-radius: 999px;
}

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

.btn-04__opt {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 22px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 650;
  color: #4a6b68;
  cursor: pointer;
  transition: color .25s;
  white-space: nowrap;
}

.btn-04__save {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .02em;
  color: var(--accent);
  background: #fff;
  padding: 3px 8px;
  border-radius: 999px;
}

.btn-04__thumb {
  position: absolute;
  z-index: 0;
  top: 5px;
  left: 5px;
  width: calc(50% - 7px);
  height: calc(100% - 10px);
  background: var(--ink);
  border-radius: 999px;
  transition: translate .35s cubic-bezier(.4,0,.2,1);
  box-shadow: 0 6px 16px -6px rgba(6,60,58,.7);
}

.btn-04__track:has(#btn-04-y:checked) .btn-04__thumb {
  translate: calc(100% + 2px) 0;
}

.btn-04__track:has(#btn-04-m:checked) label[for=btn-04-m],
.btn-04__track:has(#btn-04-y:checked) label[for=btn-04-y] {
  color: #fff;
}

.btn-04__track:has(#btn-04-y:checked) label[for=btn-04-y] .btn-04__save {
  color: var(--ink);
}

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

.btn-04__plans {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.btn-04__plan {
  border: 1.5px solid #e2eeeb;
  border-radius: 16px;
  padding: 20px 18px;
}

.btn-04__plan--hot {
  border-color: var(--accent);
  background: linear-gradient(180deg,#f3fbf9,#fff);
}

.btn-04__name {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent);
}

.btn-04__price {
  position: relative;
  height: 44px;
  margin: 8px 0 4px;
  color: var(--ink);
}

.btn-04__price span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: baseline;
  font-size: 15px;
  color: #5b7a77;
  transition: opacity .3s;
}

.btn-04__price b {
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--ink);
  margin-right: 2px;
}

.btn-04__yr {
  opacity: 0;
}

.btn-04__card:has(#btn-04-y:checked) .btn-04__mo {
  opacity: 0;
}

.btn-04__card:has(#btn-04-y:checked) .btn-04__yr {
  opacity: 1;
}

.btn-04__note {
  font-size: 13px;
  line-height: 1.5;
  color: #7a938f;
}

@media (max-width: 520px) {
  .btn-04__plans {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-04__thumb,
    .btn-04__price span,
    .btn-04__opt {
    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 Pricing Toggle with Monthly / Yearly Switch (Pure CSS, No JavaScript)
Source: https://codefronts.com/components/css-button-groups/css-pricing-toggle-with-monthly-yearly-switch-pure-css-no-javascript/

The SaaS pricing-page staple: a two-option billing switch where flipping between Monthly and Yearly slides a thumb across the track AND swaps every price on the card — the exact 'CSS pricing toggle monthly yearly switch without JavaScript' pattern product teams in the US, UK, Canada and Australia ship on landing pages. Real radio inputs, keyboard-operable, screen-reader friendly.
## HTML
```html
<section class="btn-04" aria-label="Monthly yearly pricing toggle demo">
  <div class="btn-04__card">
    <div class="btn-04__toggle" role="group" aria-label="Billing period">
      <div class="btn-04__track">
        <input type="radio" name="btn-04" id="btn-04-m" class="btn-04__in" checked>
        <label for="btn-04-m" class="btn-04__opt">Monthly</label>
        <input type="radio" name="btn-04" id="btn-04-y" class="btn-04__in">
        <label for="btn-04-y" class="btn-04__opt">Yearly <span class="btn-04__save">Save 20%</span></label>
        <span class="btn-04__thumb" aria-hidden="true"></span>
      </div>
    </div>
    <div class="btn-04__plans">
      <div class="btn-04__plan">
        <p class="btn-04__name">Starter</p>
        <p class="btn-04__price" aria-live="polite"><span class="btn-04__mo"><b>$12</b>/mo</span><span class="btn-04__yr"><b>$115</b>/yr</span></p>
        <p class="btn-04__note">Billed per seat, cancel anytime.</p>
      </div>
      <div class="btn-04__plan btn-04__plan--hot">
        <p class="btn-04__name">Studio</p>
        <p class="btn-04__price" aria-live="polite"><span class="btn-04__mo"><b>$29</b>/mo</span><span class="btn-04__yr"><b>$278</b>/yr</span></p>
        <p class="btn-04__note">Everything in Starter, unlimited projects.</p>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
.btn-04,
.btn-04 *,
.btn-04 *::before,
.btn-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-04 {
  --accent: oklch(0.66 0.15 195);
  --ink: #0d2b2e;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 120% at 50% 0%,#eafaf6,#d6efe9);
  padding: 44px 20px;
}

.btn-04__card {
  width: min(560px,100%);
  background: #fff;
  border-radius: 24px;
  padding: 32px 30px 34px;
  box-shadow: 0 30px 70px -40px rgba(6,60,58,.5);
}

.btn-04__toggle {
  display: flex;
  justify-content: center;
  margin-bottom: 26px;
}

.btn-04__track {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 5px;
  background: #e4f1ee;
  border-radius: 999px;
}

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

.btn-04__opt {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 22px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 650;
  color: #4a6b68;
  cursor: pointer;
  transition: color .25s;
  white-space: nowrap;
}

.btn-04__save {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .02em;
  color: var(--accent);
  background: #fff;
  padding: 3px 8px;
  border-radius: 999px;
}

.btn-04__thumb {
  position: absolute;
  z-index: 0;
  top: 5px;
  left: 5px;
  width: calc(50% - 7px);
  height: calc(100% - 10px);
  background: var(--ink);
  border-radius: 999px;
  transition: translate .35s cubic-bezier(.4,0,.2,1);
  box-shadow: 0 6px 16px -6px rgba(6,60,58,.7);
}

.btn-04__track:has(#btn-04-y:checked) .btn-04__thumb {
  translate: calc(100% + 2px) 0;
}

.btn-04__track:has(#btn-04-m:checked) label[for=btn-04-m],
.btn-04__track:has(#btn-04-y:checked) label[for=btn-04-y] {
  color: #fff;
}

.btn-04__track:has(#btn-04-y:checked) label[for=btn-04-y] .btn-04__save {
  color: var(--ink);
}

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

.btn-04__plans {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.btn-04__plan {
  border: 1.5px solid #e2eeeb;
  border-radius: 16px;
  padding: 20px 18px;
}

.btn-04__plan--hot {
  border-color: var(--accent);
  background: linear-gradient(180deg,#f3fbf9,#fff);
}

.btn-04__name {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent);
}

.btn-04__price {
  position: relative;
  height: 44px;
  margin: 8px 0 4px;
  color: var(--ink);
}

.btn-04__price span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: baseline;
  font-size: 15px;
  color: #5b7a77;
  transition: opacity .3s;
}

.btn-04__price b {
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--ink);
  margin-right: 2px;
}

.btn-04__yr {
  opacity: 0;
}

.btn-04__card:has(#btn-04-y:checked) .btn-04__mo {
  opacity: 0;
}

.btn-04__card:has(#btn-04-y:checked) .btn-04__yr {
  opacity: 1;
}

.btn-04__note {
  font-size: 13px;
  line-height: 1.5;
  color: #7a938f;
}

@media (max-width: 520px) {
  .btn-04__plans {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-04__thumb,
    .btn-04__price span,
    .btn-04__opt {
    transition: none;
  }
}
```

How this works

Two visually-hidden <input type=radio> (Monthly / Yearly) sit before their labels inside one role=group track. A ::after-free sliding thumb is a real <span> that the track translates with :has(): .track:has(#yearly:checked) .thumb{translate:100% 0}. No JS state — the checked radio IS the state.

The magic users love is the price swap. Each price cell holds a monthly figure and a yearly figure stacked; a container-level :has(#yearly:checked) fades one out and the other in, and a small "save 20%" badge only appears on Yearly. Because the values are real DOM text, they are crawlable and announced correctly.

.btn-04__track:has(#btn-04-y:checked) .btn-04__thumb{ translate:100% 0 }
.btn-04__card:has(#btn-04-y:checked) .btn-04__mo{ opacity:0 }
.btn-04__card:has(#btn-04-y:checked) .btn-04__yr{ opacity:1 }

Techniques used: hidden radios + label track · :has() state projection across the whole card · translate-based sliding thumb · opacity cross-fade for price swap · :focus-visible ring · prefers-reduced-motion guard

Make it yours

  • Accent + thumb color read from one --accent token.
  • Discount amount is plain text in the badge — set it to your real annual saving.
  • Thumb slide speed = the .btn-04__thumb transition duration.
  • Add a third option (e.g. Quarterly) by adding a radio + label and widening the track grid to 3 columns.

Gotchas — read before shipping

  • Keep the inputs as real radios, not divs — you get arrow-key selection, form submission and "selected" announcements for free.
  • The thumb must be a sibling of the labels inside a position:relative track, or the translate math drifts.
  • Announce the price change: put aria-live=polite on the price region so screen readers hear the new amount when the period flips.
  • :has() is Baseline 2023; if you must support older engines, a 4-line JS fallback toggling a class keeps it working.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

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

:has() selector is Baseline Newly Available (2023). Everywhere modern; older browsers can be covered with a tiny class-toggle shim.

Techniques used in this demo

Search CodeFronts

Loading…