31 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Mercury Bead Morph Button
A bead of mercury detaches from the button, stretches, and is reabsorbed on hover — genuine metaball merging from an SVG goo filter applied to plain CSS shapes. The filter is markup, the geometry is utilities, and the label sits outside the filtered layer so it stays sharp.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-31 w-full min-h-svh grid place-items-center font-tb31-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<svg aria-hidden="true" focusable="false" width="0" height="0" class="absolute size-0 overflow-hidden">
<filter id="tb-31-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="b"></feGaussianBlur>
<feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="g"></feColorMatrix>
<feBlend in="SourceGraphic" in2="g"></feBlend>
</filter>
</svg>
<button type="button" class="group relative cursor-pointer border-0 bg-transparent w-64 h-32 p-0 text-base font-bold tracking-[-0.01em] text-tb31-ink [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb31-hg focus-visible:rounded-2xl disabled:cursor-not-allowed disabled:grayscale disabled:brightness-80">
<span aria-hidden="true" class="absolute inset-0 z-10 [filter:url(#tb-31-goo)]">
<span class="tb-31__metal absolute left-9 top-10 w-[8.2rem] h-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top"></span>
<span class="tb-31__metal absolute left-9 top-10 size-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top transition-[translate,scale] duration-[750ms] ease-tb31-out group-hover:translate-x-[8.4rem] group-hover:scale-[0.62] group-focus-visible:translate-x-[8.4rem] group-focus-visible:scale-[0.62] group-active:scale-50 motion-reduce:transition-none"></span>
</span>
<span class="absolute z-30 left-[3.9rem] top-1/2 -translate-y-1/2 transition-[translate,color] duration-500 ease-tb31-out group-hover:translate-x-[0.4rem] motion-reduce:transition-none">Liquify</span>
</button>
</div><div class="tb-31 w-full min-h-svh grid place-items-center font-tb31-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<svg aria-hidden="true" focusable="false" width="0" height="0" class="absolute size-0 overflow-hidden">
<filter id="tb-31-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="b"></feGaussianBlur>
<feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="g"></feColorMatrix>
<feBlend in="SourceGraphic" in2="g"></feBlend>
</filter>
</svg>
<button type="button" class="group relative cursor-pointer border-0 bg-transparent w-64 h-32 p-0 text-base font-bold tracking-[-0.01em] text-tb31-ink [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb31-hg focus-visible:rounded-2xl disabled:cursor-not-allowed disabled:grayscale disabled:brightness-80">
<span aria-hidden="true" class="absolute inset-0 z-10 [filter:url(#tb-31-goo)]">
<span class="tb-31__metal absolute left-9 top-10 w-[8.2rem] h-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top"></span>
<span class="tb-31__metal absolute left-9 top-10 size-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top transition-[translate,scale] duration-[750ms] ease-tb31-out group-hover:translate-x-[8.4rem] group-hover:scale-[0.62] group-focus-visible:translate-x-[8.4rem] group-focus-visible:scale-[0.62] group-active:scale-50 motion-reduce:transition-none"></span>
</span>
<span class="absolute z-30 left-[3.9rem] top-1/2 -translate-y-1/2 transition-[translate,color] duration-500 ease-tb31-out group-hover:translate-x-[0.4rem] motion-reduce:transition-none">Liquify</span>
</button>
</div>@import "tailwindcss";
@theme {
--color-tb31-bg: #0c0e12;
--color-tb31-bg: oklch(0.15 0.012 260);
--color-tb31-hg: #c9d2dc;
--color-tb31-hg: oklch(0.84 0.015 250);
--color-tb31-hg-lo: #5c6672;
--color-tb31-hg-lo: oklch(0.48 0.02 250);
--color-tb31-ink: #0b0d11;
--color-tb31-ink: oklch(0.14 0.01 260);
--font-tb31-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb31-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-31 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--color-tb31-bg);
}
/* Both shapes paint the SAME gradient at the SAME size, so at rest the bead is
pixel-identical to the pill's left cap and disappears into it completely.
The goo threshold merges silhouettes, not tones. */
.tb-31__metal {
background-image: radial-gradient(120% 120% at 30% 20%, #ffffff, transparent 55%), linear-gradient(160deg, var(--color-tb31-hg), var(--color-tb31-hg-lo));
}
@media (prefers-color-scheme: light) {
.tb-31:not([data-theme="dark"]) {
--color-tb31-bg: oklch(0.92 0.006 260);
}
}
.tb-31[data-theme="light"] {
--color-tb31-bg: oklch(0.92 0.006 260);
}
@media (forced-colors: active) {
.tb-31 button > span:first-of-type {
display: none;
}
.tb-31 button {
border: 1px solid ButtonText;
background: ButtonFace;
width: auto;
height: auto;
padding: 1rem 2rem;
}
.tb-31 button > span:last-child {
position: static;
translate: none;
color: ButtonText;
}
}@import "tailwindcss";
@theme {
--color-tb31-bg: #0c0e12;
--color-tb31-bg: oklch(0.15 0.012 260);
--color-tb31-hg: #c9d2dc;
--color-tb31-hg: oklch(0.84 0.015 250);
--color-tb31-hg-lo: #5c6672;
--color-tb31-hg-lo: oklch(0.48 0.02 250);
--color-tb31-ink: #0b0d11;
--color-tb31-ink: oklch(0.14 0.01 260);
--font-tb31-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb31-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-31 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--color-tb31-bg);
}
/* Both shapes paint the SAME gradient at the SAME size, so at rest the bead is
pixel-identical to the pill's left cap and disappears into it completely.
The goo threshold merges silhouettes, not tones. */
.tb-31__metal {
background-image: radial-gradient(120% 120% at 30% 20%, #ffffff, transparent 55%), linear-gradient(160deg, var(--color-tb31-hg), var(--color-tb31-hg-lo));
}
@media (prefers-color-scheme: light) {
.tb-31:not([data-theme="dark"]) {
--color-tb31-bg: oklch(0.92 0.006 260);
}
}
.tb-31[data-theme="light"] {
--color-tb31-bg: oklch(0.92 0.006 260);
}
@media (forced-colors: active) {
.tb-31 button > span:first-of-type {
display: none;
}
.tb-31 button {
border: 1px solid ButtonText;
background: ButtonFace;
width: auto;
height: auto;
padding: 1rem 2rem;
}
.tb-31 button > span:last-child {
position: static;
translate: none;
color: ButtonText;
}
}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";
@theme {
--color-tb31-bg: #0c0e12;
--color-tb31-bg: oklch(0.15 0.012 260);
--color-tb31-hg: #c9d2dc;
--color-tb31-hg: oklch(0.84 0.015 250);
--color-tb31-hg-lo: #5c6672;
--color-tb31-hg-lo: oklch(0.48 0.02 250);
--color-tb31-ink: #0b0d11;
--color-tb31-ink: oklch(0.14 0.01 260);
--font-tb31-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb31-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-31 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--color-tb31-bg);
}
/* Both shapes paint the SAME gradient at the SAME size, so at rest the bead is
pixel-identical to the pill's left cap and disappears into it completely.
The goo threshold merges silhouettes, not tones. */
.tb-31__metal {
background-image: radial-gradient(120% 120% at 30% 20%, #ffffff, transparent 55%), linear-gradient(160deg, var(--color-tb31-hg), var(--color-tb31-hg-lo));
}
@media (prefers-color-scheme: light) {
.tb-31:not([data-theme="dark"]) {
--color-tb31-bg: oklch(0.92 0.006 260);
}
}
.tb-31[data-theme="light"] {
--color-tb31-bg: oklch(0.92 0.006 260);
}
@media (forced-colors: active) {
.tb-31 button > span:first-of-type {
display: none;
}
.tb-31 button {
border: 1px solid ButtonText;
background: ButtonFace;
width: auto;
height: auto;
padding: 1rem 2rem;
}
.tb-31 button > span:last-child {
position: static;
translate: none;
color: ButtonText;
}
}@import "tailwindcss";
@theme {
--color-tb31-bg: #0c0e12;
--color-tb31-bg: oklch(0.15 0.012 260);
--color-tb31-hg: #c9d2dc;
--color-tb31-hg: oklch(0.84 0.015 250);
--color-tb31-hg-lo: #5c6672;
--color-tb31-hg-lo: oklch(0.48 0.02 250);
--color-tb31-ink: #0b0d11;
--color-tb31-ink: oklch(0.14 0.01 260);
--font-tb31-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb31-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-31 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--color-tb31-bg);
}
/* Both shapes paint the SAME gradient at the SAME size, so at rest the bead is
pixel-identical to the pill's left cap and disappears into it completely.
The goo threshold merges silhouettes, not tones. */
.tb-31__metal {
background-image: radial-gradient(120% 120% at 30% 20%, #ffffff, transparent 55%), linear-gradient(160deg, var(--color-tb31-hg), var(--color-tb31-hg-lo));
}
@media (prefers-color-scheme: light) {
.tb-31:not([data-theme="dark"]) {
--color-tb31-bg: oklch(0.92 0.006 260);
}
}
.tb-31[data-theme="light"] {
--color-tb31-bg: oklch(0.92 0.006 260);
}
@media (forced-colors: active) {
.tb-31 button > span:first-of-type {
display: none;
}
.tb-31 button {
border: 1px solid ButtonText;
background: ButtonFace;
width: auto;
height: auto;
padding: 1rem 2rem;
}
.tb-31 button > span:last-child {
position: static;
translate: none;
color: ButtonText;
}
}<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=Space+Grotesk:wght@500;600;700&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=Space+Grotesk:wght@500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: the filter reference, both shapes' geometry, the identical background sizing that makes the bead invisible at rest, the travel-and-pinch hover, the label's parallax and its
z-30. CSS: the chrome gradient shared by both shapes — one recipe, two elements. [filter:url(#tb-31-goo)]is a good reminder that not everything in a design is a style. The filter is a document resource with an id, and the utility's job is only to point at it.- The shared
.tb-31__metalclass exists because the pill and the bead must be identical in paint. Two copies of the gradient as arbitrary utilities would be two places for them to drift apart — and the drift is invisible until it shows as a disc behind the label. - A11y fix from the CSS original: the focus ring was
outline-offset:2pxon a transparent 16×8rem button, so the ring drew a large rectangle far from the visible pill. The port keeps the geometry but also fires the bead animation ongroup-focus-visible:, so a keyboard user sees the same state change a mouse user does — previously hover-only.
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 Mercury Bead Morph Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-mercury-bead-morph-button/
A bead of mercury detaches from the button, stretches, and is reabsorbed on hover — genuine metaball merging from an SVG goo filter applied to plain CSS shapes. The filter is markup, the geometry is utilities, and the label sits outside the filtered layer so it stays sharp.
## HTML
```html
<div class="tb-31 w-full min-h-svh grid place-items-center font-tb31-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<svg aria-hidden="true" focusable="false" width="0" height="0" class="absolute size-0 overflow-hidden">
<filter id="tb-31-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="b"></feGaussianBlur>
<feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="g"></feColorMatrix>
<feBlend in="SourceGraphic" in2="g"></feBlend>
</filter>
</svg>
<button type="button" class="group relative cursor-pointer border-0 bg-transparent w-64 h-32 p-0 text-base font-bold tracking-[-0.01em] text-tb31-ink [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb31-hg focus-visible:rounded-2xl disabled:cursor-not-allowed disabled:grayscale disabled:brightness-80">
<span aria-hidden="true" class="absolute inset-0 z-10 [filter:url(#tb-31-goo)]">
<span class="tb-31__metal absolute left-9 top-10 w-[8.2rem] h-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top"></span>
<span class="tb-31__metal absolute left-9 top-10 size-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top transition-[translate,scale] duration-[750ms] ease-tb31-out group-hover:translate-x-[8.4rem] group-hover:scale-[0.62] group-focus-visible:translate-x-[8.4rem] group-focus-visible:scale-[0.62] group-active:scale-50 motion-reduce:transition-none"></span>
</span>
<span class="absolute z-30 left-[3.9rem] top-1/2 -translate-y-1/2 transition-[translate,color] duration-500 ease-tb31-out group-hover:translate-x-[0.4rem] motion-reduce:transition-none">Liquify</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb31-bg: #0c0e12;
--color-tb31-bg: oklch(0.15 0.012 260);
--color-tb31-hg: #c9d2dc;
--color-tb31-hg: oklch(0.84 0.015 250);
--color-tb31-hg-lo: #5c6672;
--color-tb31-hg-lo: oklch(0.48 0.02 250);
--color-tb31-ink: #0b0d11;
--color-tb31-ink: oklch(0.14 0.01 260);
--font-tb31-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb31-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-31 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--color-tb31-bg);
}
/* Both shapes paint the SAME gradient at the SAME size, so at rest the bead is
pixel-identical to the pill's left cap and disappears into it completely.
The goo threshold merges silhouettes, not tones. */
.tb-31__metal {
background-image: radial-gradient(120% 120% at 30% 20%, #ffffff, transparent 55%), linear-gradient(160deg, var(--color-tb31-hg), var(--color-tb31-hg-lo));
}
@media (prefers-color-scheme: light) {
.tb-31:not([data-theme="dark"]) {
--color-tb31-bg: oklch(0.92 0.006 260);
}
}
.tb-31[data-theme="light"] {
--color-tb31-bg: oklch(0.92 0.006 260);
}
@media (forced-colors: active) {
.tb-31 button > span:first-of-type {
display: none;
}
.tb-31 button {
border: 1px solid ButtonText;
background: ButtonFace;
width: auto;
height: auto;
padding: 1rem 2rem;
}
.tb-31 button > span:last-child {
position: static;
translate: none;
color: ButtonText;
}
}
```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 Mercury Bead Morph Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-mercury-bead-morph-button/
A bead of mercury detaches from the button, stretches, and is reabsorbed on hover — genuine metaball merging from an SVG goo filter applied to plain CSS shapes. The filter is markup, the geometry is utilities, and the label sits outside the filtered layer so it stays sharp.
## HTML
```html
<div class="tb-31 w-full min-h-svh grid place-items-center font-tb31-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<svg aria-hidden="true" focusable="false" width="0" height="0" class="absolute size-0 overflow-hidden">
<filter id="tb-31-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="b"></feGaussianBlur>
<feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="g"></feColorMatrix>
<feBlend in="SourceGraphic" in2="g"></feBlend>
</filter>
</svg>
<button type="button" class="group relative cursor-pointer border-0 bg-transparent w-64 h-32 p-0 text-base font-bold tracking-[-0.01em] text-tb31-ink [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tb31-hg focus-visible:rounded-2xl disabled:cursor-not-allowed disabled:grayscale disabled:brightness-80">
<span aria-hidden="true" class="absolute inset-0 z-10 [filter:url(#tb-31-goo)]">
<span class="tb-31__metal absolute left-9 top-10 w-[8.2rem] h-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top"></span>
<span class="tb-31__metal absolute left-9 top-10 size-[3.2rem] rounded-full bg-[length:8.2rem_3.2rem] bg-no-repeat bg-left-top transition-[translate,scale] duration-[750ms] ease-tb31-out group-hover:translate-x-[8.4rem] group-hover:scale-[0.62] group-focus-visible:translate-x-[8.4rem] group-focus-visible:scale-[0.62] group-active:scale-50 motion-reduce:transition-none"></span>
</span>
<span class="absolute z-30 left-[3.9rem] top-1/2 -translate-y-1/2 transition-[translate,color] duration-500 ease-tb31-out group-hover:translate-x-[0.4rem] motion-reduce:transition-none">Liquify</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb31-bg: #0c0e12;
--color-tb31-bg: oklch(0.15 0.012 260);
--color-tb31-hg: #c9d2dc;
--color-tb31-hg: oklch(0.84 0.015 250);
--color-tb31-hg-lo: #5c6672;
--color-tb31-hg-lo: oklch(0.48 0.02 250);
--color-tb31-ink: #0b0d11;
--color-tb31-ink: oklch(0.14 0.01 260);
--font-tb31-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb31-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-31 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--color-tb31-bg);
}
/* Both shapes paint the SAME gradient at the SAME size, so at rest the bead is
pixel-identical to the pill's left cap and disappears into it completely.
The goo threshold merges silhouettes, not tones. */
.tb-31__metal {
background-image: radial-gradient(120% 120% at 30% 20%, #ffffff, transparent 55%), linear-gradient(160deg, var(--color-tb31-hg), var(--color-tb31-hg-lo));
}
@media (prefers-color-scheme: light) {
.tb-31:not([data-theme="dark"]) {
--color-tb31-bg: oklch(0.92 0.006 260);
}
}
.tb-31[data-theme="light"] {
--color-tb31-bg: oklch(0.92 0.006 260);
}
@media (forced-colors: active) {
.tb-31 button > span:first-of-type {
display: none;
}
.tb-31 button {
border: 1px solid ButtonText;
background: ButtonFace;
width: auto;
height: auto;
padding: 1rem 2rem;
}
.tb-31 button > span:last-child {
position: static;
translate: none;
color: ButtonText;
}
}
```How this works
The filter is two primitives and stays in the markup. Blur everything together, then push alpha through a steep curve so overlapping blurs snap into one continuous shape:
<feGaussianBlur stdDeviation="7" result="b"/>
<feColorMatrix in="b" values="… 0 0 0 20 -9" result="g"/>
<feBlend in="SourceGraphic" in2="g"/>The last matrix row is the trick: alpha × 20 − 9 turns a soft blur into a hard edge. Tailwind's only contribution is [filter:url(#tb-31-goo)] — and that is the right division, because an SVG filter is a document resource, not a style.
Two details make the illusion hold, and both are utilities: the bead matches the pill's cap exactly at rest, and both shapes paint the same gradient at the same bg-[length:…]. The goo threshold merges silhouettes, not tones — a lighter bead shows through as a disc behind the label.
Make it yours
- Travel and size live in the hover pair: shorter throw + larger scale reads as thicker liquid.
- Raise
stdDeviationfor a gloopier merge — the alpha multiplier must rise with it or edges soften. - Recolour with
--color-tb31-hg; keep the highlight near-white or it stops looking metallic. - A second bead with a
delay-*makes a chain of droplets. - The filter id is namespaced (
tb-31-goo) — keep it that way if you paste two demos on one page.
Gotchas — read before shipping
- Never apply the goo filter to an element containing text.
feGaussianBlurblurs glyphs and thresholding cannot recover them. - SVG filters force a software raster pass on some GPUs. One instance is fine; twenty in a list is not.
- The filter id must be unique per document — paint-server references are global.
filtercreates a containing block and clips at the filter region. Give the wrapper room or the bead is cut off mid-flight.- A bead that hides at rest must match the shape it hides in — same size and same painted gradient, not merely the same colour.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
SVG filters via filter:url() work in all modern engines; feColorMatrix threshold rendering varies slightly between Blink, WebKit and Gecko — verify the exact values.