25 Tailwind CSS Button Groups
24 / 25

All 25 in 25 Tailwind CSS Button Groups

  1. 01Tailwind Split Button with Anchor-Positioned Popover Dropdown (No JS)
  2. 02Tailwind Segmented Control with Motion-Path Comet Indicator
  3. 03Tailwind Toggle Button Group with View Transitions Morphing Highlight
  4. 04Tailwind Pricing Toggle — Monthly / Yearly Billing Switch (No JS)
  5. 05Tailwind Segmented Control with Sliding Thumb (iOS Style)
  6. 06Tailwind Split Button Dropdown with Native Details Menu (Save Options)
  7. 07Tailwind Icon Button Group Toolbar with Tooltips (Rich Text Editor)
  8. 08Tailwind Pill Filter Button Group with Sliding Indicator (Anchor Positioning)
  9. 09Tailwind Toggle Button Group for Text Alignment (Radio Inputs, Live Preview)
  10. 10Tailwind Web3 Connect Wallet Button Grid (Wallet Sign-In Modal)
  11. 11Tailwind AI Model Selector Button Group (LLM Model Picker with Badges)
  12. 12Tailwind Liquid Metal Segmented Control (Animated Chrome Thumb)
  13. 13Tailwind Tab Button Group with Subgrid Underline Indicator
  14. 14Tailwind Multi-Select Chip Buttons (Tag Filter Checkboxes with Count)
  15. 15Tailwind Animated Split Button Deploy Menu (Staggered Dropdown Reveal)
  16. 16Tailwind Radio Button Group with Conic Fill Animation (Export Format Picker)
  17. 17Tailwind Retro LED Pixel Button Group (Dot-Matrix Display Buttons)
  18. 18Tailwind Hold-to-Confirm Button Group with Gradient Progress (Press and Hold)
  19. 19Tailwind Holographic Icon Segmented Control (Grid / List View Switcher)
  20. 20Tailwind 3D Vertical Button Group with Hover Depth (Perspective Stack)
  21. 21Tailwind Floating Action Button with Radial Speed Dial Menu (No JS)
  22. 22Tailwind Glitch Effect Button Group (Cyberpunk RGB-Split Audio Filter)
  23. 23Tailwind Animated Gradient Border Pill Buttons (Rotating Conic Ring)
  24. 24Tailwind Button Group with Ripple Click Effect (Radar Shockwave Feedback)
  25. 25Tailwind Button Group with Cursor-Following Spotlight Glow (Date Range Presets)
Tailwind + JSMIT licensed

Tailwind Button Group with Ripple Click Effect (Radar Shockwave Feedback)

A reactor-control action row with satisfying click feedback: a radar shockwave expands from exactly where you clicked, the primary action breathes with an @property-animated glow, and a console line logs each command. The CSS original only rippled from the centre while the mouse was held (quick clicks barely showed); a tiny script now sets --x/--y and re-arms data-fire:after:animate-tbg24-wave on every press.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tbg-24 w-full min-h-svh grid place-items-center bg-tbg24-bg bg-[radial-gradient(120%_120%_at_50%_40%,var(--color-tbg24-glow),transparent_70%)] px-5 py-14 font-tbg24-sans text-tbg24-ink" aria-label="Ripple click effect button group demo">
  <div data-tbg24-panel class="grid w-full max-w-[38rem] gap-4">
    <p class="ml-0.5 text-xs font-bold uppercase tracking-[0.14em] text-tbg24-acc">Reactor controls</p>
    <div role="group" aria-label="Reactor controls" class="flex flex-wrap gap-3.5">
      <button type="button" data-tbg24-btn style="--c:#37f5c5" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) shadow-[0_0_var(--tbg24-g)_-2px_var(--c)] animate-tbg24-breathe transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:animate-none motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Engage</button>
      <button type="button" data-tbg24-btn style="--c:#5cc8ff" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Scan</button>
      <button type="button" data-tbg24-btn style="--c:#ffb84d" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Vent</button>
      <button type="button" data-tbg24-btn style="--c:#ff6b84" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Abort</button>
    </div>
    <p role="log" aria-live="polite" data-tbg24-log class="ml-0.5 min-h-5 font-tbg24-mono text-[12.5px] text-tbg24-mut">› Standing by</p>
  </div>
</section>
@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the glow radius animatable. */

@property --tbg24-g {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@theme {
  --color-tbg24-bg: #03100e;
  --color-tbg24-glow: #072620;
  --color-tbg24-key: #0a1a17;
  --color-tbg24-ink: #eafffa;
  --color-tbg24-mut: #8fc4b6;
  --color-tbg24-mut: oklch(0.78 0.06 175);
  --color-tbg24-acc: #4fd6b8;
  --font-tbg24-sans: 'Chakra Petch', system-ui, sans-serif;
  --font-tbg24-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-tbg24-wave: tbg24-wave 0.6s ease-out;
  --animate-tbg24-breathe: tbg24-breathe 2.6s ease-in-out infinite;

  @keyframes tbg24-wave {
    0% {
      scale: 0;
      opacity: 0.65;
    }

    100% {
      scale: 2.6;
      opacity: 0;
    }
  }

  @keyframes tbg24-breathe {
    0%,
        100% {
      --tbg24-g: 14px;
    }

    50% {
      --tbg24-g: 34px;
    }
  }
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */

.tbg-24 {
  width: 100%;
  min-height: 100svh;
}
(() => {
  document.querySelectorAll('[data-tbg24-panel]').forEach((panel) => {
    if (panel.dataset.wired) return;
    panel.dataset.wired = '1';
    const log = panel.querySelector('[data-tbg24-log]');
    panel.querySelectorAll('[data-tbg24-btn]').forEach((btn) => {
      const fire = (x, y) => {
        btn.style.setProperty('--x', x);
        btn.style.setProperty('--y', y);
        btn.removeAttribute('data-fire');
        void btn.offsetWidth;
        btn.setAttribute('data-fire', '');
      };
      btn.addEventListener('pointerdown', (e) => {
        const r = btn.getBoundingClientRect();
        fire((e.clientX - r.left) + 'px', (e.clientY - r.top) + 'px');
      });
      btn.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') fire('50%', '50%');
      });
      btn.addEventListener('click', () => {
        const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
        log.textContent = '› ' + btn.textContent.trim().toUpperCase() + ' acknowledged · ' + time;
      });
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the glow radius animatable. */

@property --tbg24-g {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@theme {
  --color-tbg24-bg: #03100e;
  --color-tbg24-glow: #072620;
  --color-tbg24-key: #0a1a17;
  --color-tbg24-ink: #eafffa;
  --color-tbg24-mut: #8fc4b6;
  --color-tbg24-mut: oklch(0.78 0.06 175);
  --color-tbg24-acc: #4fd6b8;
  --font-tbg24-sans: 'Chakra Petch', system-ui, sans-serif;
  --font-tbg24-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-tbg24-wave: tbg24-wave 0.6s ease-out;
  --animate-tbg24-breathe: tbg24-breathe 2.6s ease-in-out infinite;

  @keyframes tbg24-wave {
    0% {
      scale: 0;
      opacity: 0.65;
    }

    100% {
      scale: 2.6;
      opacity: 0;
    }
  }

  @keyframes tbg24-breathe {
    0%,
        100% {
      --tbg24-g: 14px;
    }

    50% {
      --tbg24-g: 34px;
    }
  }
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */

.tbg-24 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=JetBrains+Mono:wght@400..600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • :active::after{animation:…} became data-fire:after:animate-tbg24-wave; the per-button color-mix() borders and hovers moved into arbitrary values reading the inline --c.
  • box-shadow:0 0 var(--btn-24-g) -2px var(--c) became shadow-[0_0_var(--tbg24-g)_-2px_var(--c)], still driven by @property.
  • UI upgrade: the wave now starts at the pointer and always completes — the original played only while the button was held, so quick clicks showed nothing.
  • UI upgrade: a console line confirms each command (and announces it via role="log"), and the Abort red was lifted for 5:1 on the dark key.
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 demo 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: Tailwind Button Group with Ripple Click Effect (Radar Shockwave Feedback)
Source: https://codefronts.com/components/tailwind-css-button-groups/tailwind-button-group-ripple-click-effect/

A reactor-control action row with satisfying click feedback: a radar shockwave expands from exactly where you clicked, the primary action breathes with an @property-animated glow, and a console line logs each command. The CSS original only rippled from the centre while the mouse was held (quick clicks barely showed); a tiny script now sets --x/--y and re-arms data-fire:after:animate-tbg24-wave on every press.
## HTML
```html
<section class="tbg-24 w-full min-h-svh grid place-items-center bg-tbg24-bg bg-[radial-gradient(120%_120%_at_50%_40%,var(--color-tbg24-glow),transparent_70%)] px-5 py-14 font-tbg24-sans text-tbg24-ink" aria-label="Ripple click effect button group demo">
  <div data-tbg24-panel class="grid w-full max-w-[38rem] gap-4">
    <p class="ml-0.5 text-xs font-bold uppercase tracking-[0.14em] text-tbg24-acc">Reactor controls</p>
    <div role="group" aria-label="Reactor controls" class="flex flex-wrap gap-3.5">
      <button type="button" data-tbg24-btn style="--c:#37f5c5" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) shadow-[0_0_var(--tbg24-g)_-2px_var(--c)] animate-tbg24-breathe transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:animate-none motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Engage</button>
      <button type="button" data-tbg24-btn style="--c:#5cc8ff" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Scan</button>
      <button type="button" data-tbg24-btn style="--c:#ffb84d" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Vent</button>
      <button type="button" data-tbg24-btn style="--c:#ff6b84" class="relative isolate h-14 cursor-pointer overflow-hidden rounded-2xl border-[1.5px] border-[color-mix(in_oklab,var(--c)_55%,var(--color-tbg24-key))] bg-tbg24-key px-6.5 text-[15px] font-bold tracking-[0.04em] text-(--c) transition-[color,background-color] duration-200 after:pointer-events-none after:absolute after:top-(--y) after:left-(--x) after:-z-1 after:aspect-square after:w-[120%] after:-translate-1/2 after:scale-0 after:rounded-full after:bg-[radial-gradient(circle,color-mix(in_oklab,var(--c)_70%,transparent)_0_40%,transparent_60%)] after:opacity-0 after:content-[''] hover:bg-[color-mix(in_oklab,var(--c)_16%,var(--color-tbg24-key))] hover:text-tbg24-ink active:bg-[color-mix(in_oklab,var(--c)_26%,var(--color-tbg24-key))] data-fire:after:animate-tbg24-wave focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-(--c) motion-reduce:data-fire:after:animate-none [--x:50%] [--y:50%]">Abort</button>
    </div>
    <p role="log" aria-live="polite" data-tbg24-log class="ml-0.5 min-h-5 font-tbg24-mono text-[12.5px] text-tbg24-mut">› Standing by</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* @property is an at-rule with no utility: it makes the glow radius animatable. */

@property --tbg24-g {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@theme {
  --color-tbg24-bg: #03100e;
  --color-tbg24-glow: #072620;
  --color-tbg24-key: #0a1a17;
  --color-tbg24-ink: #eafffa;
  --color-tbg24-mut: #8fc4b6;
  --color-tbg24-mut: oklch(0.78 0.06 175);
  --color-tbg24-acc: #4fd6b8;
  --font-tbg24-sans: 'Chakra Petch', system-ui, sans-serif;
  --font-tbg24-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-tbg24-wave: tbg24-wave 0.6s ease-out;
  --animate-tbg24-breathe: tbg24-breathe 2.6s ease-in-out infinite;

  @keyframes tbg24-wave {
    0% {
      scale: 0;
      opacity: 0.65;
    }

    100% {
      scale: 2.6;
      opacity: 0;
    }
  }

  @keyframes tbg24-breathe {
    0%,
        100% {
      --tbg24-g: 14px;
    }

    50% {
      --tbg24-g: 34px;
    }
  }
}
/* Root rule: the full-bleed contract, restated so the stage is sized before the browser build compiles. */

.tbg-24 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(() => {
  document.querySelectorAll('[data-tbg24-panel]').forEach((panel) => {
    if (panel.dataset.wired) return;
    panel.dataset.wired = '1';
    const log = panel.querySelector('[data-tbg24-log]');
    panel.querySelectorAll('[data-tbg24-btn]').forEach((btn) => {
      const fire = (x, y) => {
        btn.style.setProperty('--x', x);
        btn.style.setProperty('--y', y);
        btn.removeAttribute('data-fire');
        void btn.offsetWidth;
        btn.setAttribute('data-fire', '');
      };
      btn.addEventListener('pointerdown', (e) => {
        const r = btn.getBoundingClientRect();
        fire((e.clientX - r.left) + 'px', (e.clientY - r.top) + 'px');
      });
      btn.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') fire('50%', '50%');
      });
      btn.addEventListener('click', () => {
        const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
        log.textContent = '› ' + btn.textContent.trim().toUpperCase() + ' acknowledged · ' + time;
      });
    });
  });
})();
```

How this works

Re-arming an animation. CSS won't replay an animation that's already applied, so the script toggles the attribute with a reflow in between:

btn.style.setProperty('--x', x + 'px');
btn.removeAttribute('data-fire');
void btn.offsetWidth;
btn.setAttribute('data-fire', '');

Keyboard activation (Enter/Space) has no pointer position, so the ripple starts from the centre. The wave runs to completion even on the briefest tap.

Make it yours

  • Bigger wave: the scale: 2.6 end value in tbg24-wave.
  • Button colours: each button's inline --c.
  • No log: delete the console paragraph and the two log lines in the script.

Gotchas — read before shipping

  • Keep overflow-hidden and isolate on buttons, or the wave escapes and slides under siblings.
  • Destructive actions (Abort) deserve a confirmation step in production.
  • The ripple is decorative; state changes must also be communicated in text (the log).

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

@property is Baseline 2024; without it the glow is static. Tailwind v4 sets the floor.

Techniques used in this demo

Plain CSSno Tailwind surface

HTML & JSno Tailwind involved

Search CodeFronts

Loading…