04 / 47
All 47 in 47 Tailwind CSS Toggles & Switches
- 01Tailwind iOS-Style Toggle Switch with appearance-none
- 02Tailwind Dark Mode Sun / Moon Theme Switcher
- 03Tailwind Neumorphic Soft-UI Pill Toggle
- 04Tailwind Gooey Liquid Morphing Switch with SVG Filter
- 05Tailwind Bouncy Elastic Physics Toggle (Overshoot Cubic-Bezier)
- 06Tailwind ON / OFF Text Switch with Labels Inside the Track
- 07Tailwind SaaS Pricing Toggle — Monthly / Annually Segmented Pill
- 08Tailwind Mechanical Keycap Toggles with Haptic Feedback
- 09Tailwind Waveform Mute Toggle with Animated EQ Bars
- 10Tailwind Timed Auto-Off Toggle with Conic Countdown Ring
- 11Tailwind Press-and-Hold to Confirm Toggle (Fingerprint Scan)
- 12Tailwind Split-Flap Departure Board Toggle (3D Flip)
- 13Tailwind Permission Toggle Group with :has() Live Status Badge
- 14Tailwind Material Design 3 Switch with State Layer
- 15Tailwind Skeuomorphic Wall Light Switch with Room Relight
- 16Tailwind Neon Cyberpunk Toggle with Clipped Corners
- 17Tailwind Brutalist Snap Toggle with Hard Offset Shadow
- 18Tailwind Day / Night Landscape Scene Switch
- 19Tailwind 3D Flip Card Toggle (rotateY Switch)
- 20Tailwind Sliding Pill Toggle with Label Inside the Thumb
- 21Tailwind Glassmorphism Frosted Toggle with backdrop-blur
- 22Tailwind Rotary Knob Toggle with Brushed-Metal Conic Gradient
- 23Tailwind iOS Settings List with Grouped Toggle Rows
- 24Tailwind Minimal Outline Toggle Drawn in currentColor
- 25Tailwind Squash-and-Stretch Pebble Toggle
- 26Tailwind Industrial Rocker I/O Power Switch
- 27Tailwind Accessible role=switch Toggle with forced-colors Support
- 28Tailwind Macro-Pad Toggle Grid with RGB Underglow
- 29Tailwind Radial VU Meter Mute Toggle
- 30Tailwind Segmented Pip Countdown Auto-Off Toggle
- 31Tailwind Slide-to-Confirm Toggle Rail (Drag or Arrow Keys)
- 32Tailwind Odometer Roll ON / OFF Toggle
- 33Tailwind Master Toggle with Indeterminate Tri-State
- 34Tailwind iOS Control Centre Tile Toggles
- 35Tailwind Material 3 Connected Icon Toggle Buttons
- 36Tailwind Two-Gang Wall Plate with Cumulative Room Light
- 37Tailwind Neon Toggle with Animated Conic Border Light
- 38Tailwind Brutalist Rubber-Stamp Toggle
- 39Tailwind Aurora Night Sky Toggle Switch
- 40Tailwind 3D Rotating Cube Toggle
- 41Tailwind Three-Position Off / Auto / On Selector
- 42Tailwind Liquid Glass Toggle with Specular Highlight
- 43Tailwind Three-Detent Rotary Selector Knob
- 44Tailwind Hardware Slide Switch with Knurled Grip
- 45Tailwind Battery Saver Toggle with Live Fill Meter
- 46Tailwind Morphing Sun-to-Moon Icon Toggle (SVG Stroke)
- 47Tailwind Guarded Kill Switch Under a Flip-Up Safety Cover
Tailwind Gooey Liquid Morphing Switch with SVG Filter
The blob that necks and snaps: an inline feGaussianBlur + feColorMatrix chain fuses a thumb with a trailing echo, and the only thing Tailwind has to do is move them — has-checked: on the track, a delay-[90ms] on the echo, and the filter draws the liquid between them.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-04 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg04-bg font-ttg04-sans" aria-label="Gooey liquid morphing switch">
<svg width="0" height="0" aria-hidden="true" class="absolute">
<defs><filter id="ttg04-goo"><feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="g"/><feComposite in="SourceGraphic" in2="g" operator="atop"/></filter></defs>
</svg>
<label for="ttg-04-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg04-card border border-ttg04-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg04-ink">Liquid mode</span>
<span class="text-[12.5px] text-ttg04-mute">Metaball transitions between views</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg04-track shadow-[inset_0_3px_8px_oklch(0.15_0.03_300/0.6)] has-checked:[&_.ttg04-blob]:translate-x-12 has-checked:[&_.ttg04-echo]:translate-x-12">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none [filter:url(#ttg04-goo)]">
<i class="ttg04-blob absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:duration-200"></i>
<i class="ttg04-echo absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo scale-[0.86] transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] delay-[90ms] motion-reduce:duration-200 motion-reduce:delay-0"></i>
</span>
<input type="checkbox" id="ttg-04-sw" role="switch" class="appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg04-goo focus-visible:outline-offset-4">
</span>
</label>
<p class="font-ttg04-mono text-[11px] tracking-[0.14em] uppercase text-ttg04-mute">Watch the drop neck & snap</p>
</section><section class="ttg-04 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg04-bg font-ttg04-sans" aria-label="Gooey liquid morphing switch">
<svg width="0" height="0" aria-hidden="true" class="absolute">
<defs><filter id="ttg04-goo"><feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="g"/><feComposite in="SourceGraphic" in2="g" operator="atop"/></filter></defs>
</svg>
<label for="ttg-04-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg04-card border border-ttg04-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg04-ink">Liquid mode</span>
<span class="text-[12.5px] text-ttg04-mute">Metaball transitions between views</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg04-track shadow-[inset_0_3px_8px_oklch(0.15_0.03_300/0.6)] has-checked:[&_.ttg04-blob]:translate-x-12 has-checked:[&_.ttg04-echo]:translate-x-12">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none [filter:url(#ttg04-goo)]">
<i class="ttg04-blob absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:duration-200"></i>
<i class="ttg04-echo absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo scale-[0.86] transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] delay-[90ms] motion-reduce:duration-200 motion-reduce:delay-0"></i>
</span>
<input type="checkbox" id="ttg-04-sw" role="switch" class="appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg04-goo focus-visible:outline-offset-4">
</span>
</label>
<p class="font-ttg04-mono text-[11px] tracking-[0.14em] uppercase text-ttg04-mute">Watch the drop neck & snap</p>
</section>@import "tailwindcss";
@theme {
--color-ttg04-bg: #16111f;
--color-ttg04-bg: oklch(0.19 0.03 300);
--color-ttg04-card: #1e1730;
--color-ttg04-card: oklch(0.24 0.04 300);
--color-ttg04-edge: #2f2547;
--color-ttg04-edge: oklch(0.32 0.05 300);
--color-ttg04-track: #241b38;
--color-ttg04-track: oklch(0.27 0.05 300);
--color-ttg04-goo: #e878b8;
--color-ttg04-goo: oklch(0.72 0.19 340);
--color-ttg04-ink: #ded5f0;
--color-ttg04-ink: oklch(0.9 0.03 300);
--color-ttg04-mute: #9b8fba;
--color-ttg04-mute: oklch(0.68 0.05 300);
--font-ttg04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg04-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.ttg-04 {
width: 100%;
min-height: 100svh;
}
.ttg-04 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The scale on the echo blob must survive its translate; Tailwind composes both through
its own transform variables, but the filtered layer is re-rasterised each frame in
Safari, so pin it to its own compositing layer. */
.ttg-04 .ttg04-blob,
.ttg-04 .ttg04-echo {
will-change: transform;
}@import "tailwindcss";
@theme {
--color-ttg04-bg: #16111f;
--color-ttg04-bg: oklch(0.19 0.03 300);
--color-ttg04-card: #1e1730;
--color-ttg04-card: oklch(0.24 0.04 300);
--color-ttg04-edge: #2f2547;
--color-ttg04-edge: oklch(0.32 0.05 300);
--color-ttg04-track: #241b38;
--color-ttg04-track: oklch(0.27 0.05 300);
--color-ttg04-goo: #e878b8;
--color-ttg04-goo: oklch(0.72 0.19 340);
--color-ttg04-ink: #ded5f0;
--color-ttg04-ink: oklch(0.9 0.03 300);
--color-ttg04-mute: #9b8fba;
--color-ttg04-mute: oklch(0.68 0.05 300);
--font-ttg04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg04-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.ttg-04 {
width: 100%;
min-height: 100svh;
}
.ttg-04 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The scale on the echo blob must survive its translate; Tailwind composes both through
its own transform variables, but the filtered layer is re-rasterised each frame in
Safari, so pin it to its own compositing layer. */
.ttg-04 .ttg04-blob,
.ttg-04 .ttg04-echo {
will-change: transform;
}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-ttg04-bg: #16111f;
--color-ttg04-bg: oklch(0.19 0.03 300);
--color-ttg04-card: #1e1730;
--color-ttg04-card: oklch(0.24 0.04 300);
--color-ttg04-edge: #2f2547;
--color-ttg04-edge: oklch(0.32 0.05 300);
--color-ttg04-track: #241b38;
--color-ttg04-track: oklch(0.27 0.05 300);
--color-ttg04-goo: #e878b8;
--color-ttg04-goo: oklch(0.72 0.19 340);
--color-ttg04-ink: #ded5f0;
--color-ttg04-ink: oklch(0.9 0.03 300);
--color-ttg04-mute: #9b8fba;
--color-ttg04-mute: oklch(0.68 0.05 300);
--font-ttg04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg04-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.ttg-04 {
width: 100%;
min-height: 100svh;
}
.ttg-04 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The scale on the echo blob must survive its translate; Tailwind composes both through
its own transform variables, but the filtered layer is re-rasterised each frame in
Safari, so pin it to its own compositing layer. */
.ttg-04 .ttg04-blob,
.ttg-04 .ttg04-echo {
will-change: transform;
}@import "tailwindcss";
@theme {
--color-ttg04-bg: #16111f;
--color-ttg04-bg: oklch(0.19 0.03 300);
--color-ttg04-card: #1e1730;
--color-ttg04-card: oklch(0.24 0.04 300);
--color-ttg04-edge: #2f2547;
--color-ttg04-edge: oklch(0.32 0.05 300);
--color-ttg04-track: #241b38;
--color-ttg04-track: oklch(0.27 0.05 300);
--color-ttg04-goo: #e878b8;
--color-ttg04-goo: oklch(0.72 0.19 340);
--color-ttg04-ink: #ded5f0;
--color-ttg04-ink: oklch(0.9 0.03 300);
--color-ttg04-mute: #9b8fba;
--color-ttg04-mute: oklch(0.68 0.05 300);
--font-ttg04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg04-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.ttg-04 {
width: 100%;
min-height: 100svh;
}
.ttg-04 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The scale on the echo blob must survive its translate; Tailwind composes both through
its own transform variables, but the filtered layer is re-rasterised each frame in
Safari, so pin it to its own compositing layer. */
.ttg-04 .ttg04-blob,
.ttg-04 .ttg04-echo {
will-change: transform;
}<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=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&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=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
filter:url(#id)has no utility and should not have one —[filter:url(#ttg04-goo)]is the correct arbitrary property, and the namespaced id is what lets 47 demos share a page.- The checkbox is not a sibling of the blobs, so
peer-checked:cannot reach them.has-checked:[&_.ttg04-blob]:translate-x-12on the shared ancestor is the utility form of the original's.track:has(:checked) .blob— longer to type, identical output, and it sits on the element that owns both. delay-[90ms]is the whole viscosity effect. Everything else about the goo is SVG.- UI change from the CSS original: the bare switch gained a bordered card and a supporting line, the background moved off pure
#171222to an oklch violet with a lighter card behind the track (the pink blob read as floating in void), and the caption ink lifted tooklch(0.68)for 4.6:1 — it was 3.1:1.
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 Gooey Liquid Morphing Switch with SVG Filter
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-gooey-liquid-morphing-switch-svg-filter/
The blob that necks and snaps: an inline feGaussianBlur + feColorMatrix chain fuses a thumb with a trailing echo, and the only thing Tailwind has to do is move them — has-checked: on the track, a delay-[90ms] on the echo, and the filter draws the liquid between them.
## HTML
```html
<section class="ttg-04 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg04-bg font-ttg04-sans" aria-label="Gooey liquid morphing switch">
<svg width="0" height="0" aria-hidden="true" class="absolute">
<defs><filter id="ttg04-goo"><feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="g"/><feComposite in="SourceGraphic" in2="g" operator="atop"/></filter></defs>
</svg>
<label for="ttg-04-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg04-card border border-ttg04-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg04-ink">Liquid mode</span>
<span class="text-[12.5px] text-ttg04-mute">Metaball transitions between views</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg04-track shadow-[inset_0_3px_8px_oklch(0.15_0.03_300/0.6)] has-checked:[&_.ttg04-blob]:translate-x-12 has-checked:[&_.ttg04-echo]:translate-x-12">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none [filter:url(#ttg04-goo)]">
<i class="ttg04-blob absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:duration-200"></i>
<i class="ttg04-echo absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo scale-[0.86] transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] delay-[90ms] motion-reduce:duration-200 motion-reduce:delay-0"></i>
</span>
<input type="checkbox" id="ttg-04-sw" role="switch" class="appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg04-goo focus-visible:outline-offset-4">
</span>
</label>
<p class="font-ttg04-mono text-[11px] tracking-[0.14em] uppercase text-ttg04-mute">Watch the drop neck & snap</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg04-bg: #16111f;
--color-ttg04-bg: oklch(0.19 0.03 300);
--color-ttg04-card: #1e1730;
--color-ttg04-card: oklch(0.24 0.04 300);
--color-ttg04-edge: #2f2547;
--color-ttg04-edge: oklch(0.32 0.05 300);
--color-ttg04-track: #241b38;
--color-ttg04-track: oklch(0.27 0.05 300);
--color-ttg04-goo: #e878b8;
--color-ttg04-goo: oklch(0.72 0.19 340);
--color-ttg04-ink: #ded5f0;
--color-ttg04-ink: oklch(0.9 0.03 300);
--color-ttg04-mute: #9b8fba;
--color-ttg04-mute: oklch(0.68 0.05 300);
--font-ttg04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg04-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.ttg-04 {
width: 100%;
min-height: 100svh;
}
.ttg-04 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The scale on the echo blob must survive its translate; Tailwind composes both through
its own transform variables, but the filtered layer is re-rasterised each frame in
Safari, so pin it to its own compositing layer. */
.ttg-04 .ttg04-blob,
.ttg-04 .ttg04-echo {
will-change: transform;
}
```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 Gooey Liquid Morphing Switch with SVG Filter
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-gooey-liquid-morphing-switch-svg-filter/
The blob that necks and snaps: an inline feGaussianBlur + feColorMatrix chain fuses a thumb with a trailing echo, and the only thing Tailwind has to do is move them — has-checked: on the track, a delay-[90ms] on the echo, and the filter draws the liquid between them.
## HTML
```html
<section class="ttg-04 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg04-bg font-ttg04-sans" aria-label="Gooey liquid morphing switch">
<svg width="0" height="0" aria-hidden="true" class="absolute">
<defs><filter id="ttg04-goo"><feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/><feColorMatrix in="b" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="g"/><feComposite in="SourceGraphic" in2="g" operator="atop"/></filter></defs>
</svg>
<label for="ttg-04-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg04-card border border-ttg04-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg04-ink">Liquid mode</span>
<span class="text-[12.5px] text-ttg04-mute">Metaball transitions between views</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg04-track shadow-[inset_0_3px_8px_oklch(0.15_0.03_300/0.6)] has-checked:[&_.ttg04-blob]:translate-x-12 has-checked:[&_.ttg04-echo]:translate-x-12">
<span aria-hidden="true" class="absolute inset-0 pointer-events-none [filter:url(#ttg04-goo)]">
<i class="ttg04-blob absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:duration-200"></i>
<i class="ttg04-echo absolute top-2 left-2 w-8 h-8 rounded-full bg-ttg04-goo scale-[0.86] transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] delay-[90ms] motion-reduce:duration-200 motion-reduce:delay-0"></i>
</span>
<input type="checkbox" id="ttg-04-sw" role="switch" class="appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg04-goo focus-visible:outline-offset-4">
</span>
</label>
<p class="font-ttg04-mono text-[11px] tracking-[0.14em] uppercase text-ttg04-mute">Watch the drop neck & snap</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg04-bg: #16111f;
--color-ttg04-bg: oklch(0.19 0.03 300);
--color-ttg04-card: #1e1730;
--color-ttg04-card: oklch(0.24 0.04 300);
--color-ttg04-edge: #2f2547;
--color-ttg04-edge: oklch(0.32 0.05 300);
--color-ttg04-track: #241b38;
--color-ttg04-track: oklch(0.27 0.05 300);
--color-ttg04-goo: #e878b8;
--color-ttg04-goo: oklch(0.72 0.19 340);
--color-ttg04-ink: #ded5f0;
--color-ttg04-ink: oklch(0.9 0.03 300);
--color-ttg04-mute: #9b8fba;
--color-ttg04-mute: oklch(0.68 0.05 300);
--font-ttg04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg04-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.ttg-04 {
width: 100%;
min-height: 100svh;
}
.ttg-04 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The scale on the echo blob must survive its translate; Tailwind composes both through
its own transform variables, but the filtered layer is re-rasterised each frame in
Safari, so pin it to its own compositing layer. */
.ttg-04 .ttg04-blob,
.ttg-04 .ttg04-echo {
will-change: transform;
}
```How this works
The goo is SVG, and it should be. A container is filtered through filter:url(#ttg04-goo), whose chain blurs everything inside it (stdDeviation="6") then slams the alpha contrast back up (feColorMatrix 18 / −7). Any two shapes that overlap while blurred fuse into one silhouette. Tailwind has no utility for this and should not: the id is a document reference.
Two blobs, one delay. The thumb and its echo run the same 500ms transform transition, but the echo carries delay-[90ms]. Mid-travel they separate by just enough for the filter to draw a stretching neck; on arrival the echo catches up and the drop snaps whole.
The input floats above the goo. It is absolute inset-0 opacity-0-adjacent (transparent background, full size), so the whole track is clickable and focusable while the visual lives in the filtered layer beneath. Because the input is not a sibling of the blobs, the state has to travel through the ancestor: has-checked: on the track, with an arbitrary descendant selector.
Make it yours
- More viscosity: raise the echo's
delay-[90ms]towarddelay-[120ms]. - Runnier goo:
stdDeviation="8"and rebalance the matrix to 20 / −9. - Recolour through
--color-ttg04-goo— the filter is colour-agnostic. - Droop at rest: give the echo a few px of
translate-yin its base class list.
Gotchas — read before shipping
- The filter must wrap ONLY the blobs. Filtering the whole switch blurs the track, the ring and the label with it.
- Keep blob edges at least one blur radius away from the filtered container's bounds, or the goo clips to a flat edge.
- Every instance needs a unique filter id. Two copies of
#gooon one page silently both resolve to the first — which is exactly why every id here is namespacedttg04-. has-checked:plus an arbitrary descendant selector is doing real work here; if you support Firefox < 121, ship the plain sliding thumb as the fallback (the filter simply does nothing).
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
filter:url() with SVG filters is supported everywhere modern; :has() (Firefox 121+) drives the slide. Without either, the control still toggles and remains fully accessible.