24 / 25
All 25 in 25 Tailwind CSS Button Groups
- 01Tailwind Split Button with Anchor-Positioned Popover Dropdown (No JS)
- 02Tailwind Segmented Control with Motion-Path Comet Indicator
- 03Tailwind Toggle Button Group with View Transitions Morphing Highlight
- 04Tailwind Pricing Toggle — Monthly / Yearly Billing Switch (No JS)
- 05Tailwind Segmented Control with Sliding Thumb (iOS Style)
- 06Tailwind Split Button Dropdown with Native Details Menu (Save Options)
- 07Tailwind Icon Button Group Toolbar with Tooltips (Rich Text Editor)
- 08Tailwind Pill Filter Button Group with Sliding Indicator (Anchor Positioning)
- 09Tailwind Toggle Button Group for Text Alignment (Radio Inputs, Live Preview)
- 10Tailwind Web3 Connect Wallet Button Grid (Wallet Sign-In Modal)
- 11Tailwind AI Model Selector Button Group (LLM Model Picker with Badges)
- 12Tailwind Liquid Metal Segmented Control (Animated Chrome Thumb)
- 13Tailwind Tab Button Group with Subgrid Underline Indicator
- 14Tailwind Multi-Select Chip Buttons (Tag Filter Checkboxes with Count)
- 15Tailwind Animated Split Button Deploy Menu (Staggered Dropdown Reveal)
- 16Tailwind Radio Button Group with Conic Fill Animation (Export Format Picker)
- 17Tailwind Retro LED Pixel Button Group (Dot-Matrix Display Buttons)
- 18Tailwind Hold-to-Confirm Button Group with Gradient Progress (Press and Hold)
- 19Tailwind Holographic Icon Segmented Control (Grid / List View Switcher)
- 20Tailwind 3D Vertical Button Group with Hover Depth (Perspective Stack)
- 21Tailwind Floating Action Button with Radial Speed Dial Menu (No JS)
- 22Tailwind Glitch Effect Button Group (Cyberpunk RGB-Split Audio Filter)
- 23Tailwind Animated Gradient Border Pill Buttons (Rotating Conic Ring)
- 24Tailwind Button Group with Ripple Click Effect (Radar Shockwave Feedback)
- 25Tailwind Button Group with Cursor-Following Spotlight Glow (Date Range Presets)
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.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
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><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;
}@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;
});
});
});
})();(() => {
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.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@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;
}@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;
}<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">
<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:…}becamedata-fire:after:animate-tbg24-wave; the per-buttoncolor-mix()borders and hovers moved into arbitrary values reading the inline--c.box-shadow:0 0 var(--btn-24-g) -2px var(--c)becameshadow-[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.
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;
});
});
});
})();
```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.6end value intbg24-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-hiddenandisolateon 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property is Baseline 2024; without it the glow is static. Tailwind v4 sets the floor.