04 / 20
All 20 in 20 Tailwind Liquid Glass Effects
- 01Tailwind Liquid Glass Card with Hover Refraction
- 02Tailwind Liquid Glass Button with Glossy Highlight
- 03Tailwind Liquid Glass Navigation Bar
- 04Tailwind Liquid Gooey Toggle Switch
- 05Tailwind Liquid Glass Modal Dialog
- 06Tailwind Liquid Glass Theme Switcher
- 07Tailwind Liquid Glass Input Form Fields
- 08Tailwind SVG Displacement Liquid Glass Background
- 09Tailwind Liquid Glass Dropdown Menu
- 10Tailwind Liquid Glass Pricing Table
- 11Tailwind Liquid Glass Tooltip Popover
- 12Tailwind Liquid Glass Audio Player UI
- 13Tailwind Liquid Glass Range Slider
- 14Tailwind Liquid Glass Toast Notification
- 15Tailwind Liquid Glass Sidebar Navigation
- 16Tailwind Liquid Glass Accordion
- 17Tailwind Liquid Glass Login Card
- 18Tailwind Liquid Glass Floating Action Button
- 19Tailwind Liquid Glass Loading Skeleton
- 20Tailwind Liquid Glass Multi-Step Form Wizard
Tailwind Liquid Gooey Toggle Switch
A handle that behaves like liquid metal: it stretches as it travels, fuses with the track blob, then snaps into place. The whole state machine is peer plus peer-checked: on a native checkbox, and the only thing utilities cannot express — the SVG goo filter and its stretch keyframe — stays in the stylesheet where it belongs.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="lgt-04 grid w-full min-h-svh place-items-center p-15 font-lgt04-sans [background:radial-gradient(circle_at_30%_20%,#2a2740,#12101d)]" aria-label="Tailwind liquid gooey toggle switch">
<svg width="0" height="0" aria-hidden="true" focusable="false" class="absolute">
<filter id="lgt-04-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 20 -9" result="goo"/>
<feBlend in="SourceGraphic" in2="goo"/>
</filter>
</svg>
<label class="inline-flex cursor-pointer items-center gap-3.5 text-[22px] text-lgt04-fg">
<input type="checkbox" checked class="peer sr-only">
<span aria-hidden="true" class="relative h-[1.7em] w-[3.4em] rounded-full border border-white/25 bg-lgt04-off backdrop-blur-[6px] transition-colors duration-350 shadow-[inset_0_2px_8px_rgba(0,0,0,.45),inset_0_1px_0_rgba(255,255,255,.25)] peer-checked:bg-lgt04-on peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-white peer-checked:[&_.lgt-04-fixed]:opacity-0 peer-checked:[&_.lgt-04-handle]:left-[2em] peer-checked:[&_.lgt-04-handle]:animate-lgt04-stretch motion-reduce:[&_.lgt-04-handle]:animate-none">
<span class="absolute inset-0 [filter:url(#lgt-04-goo)]">
<span class="lgt-04-fixed absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white opacity-90 transition-opacity delay-200 duration-300"></span>
<span class="lgt-04-handle absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white shadow-[0_2px_6px_rgba(0,0,0,.35)] transition-[left] duration-400 ease-lgt04"></span>
</span>
</span>
<span class="text-[0.85em] font-semibold">Liquid mode</span>
</label>
</section><section class="lgt-04 grid w-full min-h-svh place-items-center p-15 font-lgt04-sans [background:radial-gradient(circle_at_30%_20%,#2a2740,#12101d)]" aria-label="Tailwind liquid gooey toggle switch">
<svg width="0" height="0" aria-hidden="true" focusable="false" class="absolute">
<filter id="lgt-04-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 20 -9" result="goo"/>
<feBlend in="SourceGraphic" in2="goo"/>
</filter>
</svg>
<label class="inline-flex cursor-pointer items-center gap-3.5 text-[22px] text-lgt04-fg">
<input type="checkbox" checked class="peer sr-only">
<span aria-hidden="true" class="relative h-[1.7em] w-[3.4em] rounded-full border border-white/25 bg-lgt04-off backdrop-blur-[6px] transition-colors duration-350 shadow-[inset_0_2px_8px_rgba(0,0,0,.45),inset_0_1px_0_rgba(255,255,255,.25)] peer-checked:bg-lgt04-on peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-white peer-checked:[&_.lgt-04-fixed]:opacity-0 peer-checked:[&_.lgt-04-handle]:left-[2em] peer-checked:[&_.lgt-04-handle]:animate-lgt04-stretch motion-reduce:[&_.lgt-04-handle]:animate-none">
<span class="absolute inset-0 [filter:url(#lgt-04-goo)]">
<span class="lgt-04-fixed absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white opacity-90 transition-opacity delay-200 duration-300"></span>
<span class="lgt-04-handle absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white shadow-[0_2px_6px_rgba(0,0,0,.35)] transition-[left] duration-400 ease-lgt04"></span>
</span>
</span>
<span class="text-[0.85em] font-semibold">Liquid mode</span>
</label>
</section>@import "tailwindcss";
@theme {
--color-lgt04-on: #34d3a4;
--color-lgt04-on: oklch(0.7 0.17 165);
--color-lgt04-off: #6c6a85;
--color-lgt04-off: oklch(0.55 0.03 260);
--color-lgt04-fg: #eceaf5;
--font-lgt04-sans: 'Manrope', system-ui, sans-serif;
--ease-lgt04: cubic-bezier(0.6, -0.4, 0.4, 1.4);
--animate-lgt04-stretch: lgt-04-stretch 0.4s ease;
}
/* The two things utilities cannot express: a keyframe, and the -50% Y that
must survive inside the scaleX so the handle stays vertically centred. */
@keyframes lgt-04-stretch {
0% {
transform: translateY(-50%) scaleX(1);
}
45% {
transform: translateY(-50%) scaleX(1.55);
}
100% {
transform: translateY(-50%) scaleX(1);
}
}@import "tailwindcss";
@theme {
--color-lgt04-on: #34d3a4;
--color-lgt04-on: oklch(0.7 0.17 165);
--color-lgt04-off: #6c6a85;
--color-lgt04-off: oklch(0.55 0.03 260);
--color-lgt04-fg: #eceaf5;
--font-lgt04-sans: 'Manrope', system-ui, sans-serif;
--ease-lgt04: cubic-bezier(0.6, -0.4, 0.4, 1.4);
--animate-lgt04-stretch: lgt-04-stretch 0.4s ease;
}
/* The two things utilities cannot express: a keyframe, and the -50% Y that
must survive inside the scaleX so the handle stays vertically centred. */
@keyframes lgt-04-stretch {
0% {
transform: translateY(-50%) scaleX(1);
}
45% {
transform: translateY(-50%) scaleX(1.55);
}
100% {
transform: translateY(-50%) scaleX(1);
}
}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-lgt04-on: #34d3a4;
--color-lgt04-on: oklch(0.7 0.17 165);
--color-lgt04-off: #6c6a85;
--color-lgt04-off: oklch(0.55 0.03 260);
--color-lgt04-fg: #eceaf5;
--font-lgt04-sans: 'Manrope', system-ui, sans-serif;
--ease-lgt04: cubic-bezier(0.6, -0.4, 0.4, 1.4);
--animate-lgt04-stretch: lgt-04-stretch 0.4s ease;
}
/* The two things utilities cannot express: a keyframe, and the -50% Y that
must survive inside the scaleX so the handle stays vertically centred. */
@keyframes lgt-04-stretch {
0% {
transform: translateY(-50%) scaleX(1);
}
45% {
transform: translateY(-50%) scaleX(1.55);
}
100% {
transform: translateY(-50%) scaleX(1);
}
}@import "tailwindcss";
@theme {
--color-lgt04-on: #34d3a4;
--color-lgt04-on: oklch(0.7 0.17 165);
--color-lgt04-off: #6c6a85;
--color-lgt04-off: oklch(0.55 0.03 260);
--color-lgt04-fg: #eceaf5;
--font-lgt04-sans: 'Manrope', system-ui, sans-serif;
--ease-lgt04: cubic-bezier(0.6, -0.4, 0.4, 1.4);
--animate-lgt04-stretch: lgt-04-stretch 0.4s ease;
}
/* The two things utilities cannot express: a keyframe, and the -50% Y that
must survive inside the scaleX so the handle stays vertically centred. */
@keyframes lgt-04-stretch {
0% {
transform: translateY(-50%) scaleX(1);
}
45% {
transform: translateY(-50%) scaleX(1.55);
}
100% {
transform: translateY(-50%) scaleX(1);
}
}<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=Manrope:wght@400;600;800&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=Manrope:wght@400;600;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- A native checkbox with
peerreplaces every:checked +selector in the CSS version; the state lives in the control, not in a class. peer-checked:[&_.lgt-04-handle]:left-[2em]is the child-selector escape —peer-*reaches siblings, and the handle is a descendant of one.- The stretch keyframe becomes an
--animate-*theme entry, so the markup saysanimate-lgt04-stretchand the timing is defined once. - The SVG goo filter stays in the markup with a namespaced id, and one arbitrary
[filter:url(#lgt-04-goo)]utility attaches it to the blob layer only. sr-onlyis load-bearing: it clips the input without removing it from the focus order, which is the difference between an accessible switch and a decorative one.
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 Liquid Gooey Toggle Switch
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-gooey-toggle-switch/
A handle that behaves like liquid metal: it stretches as it travels, fuses with the track blob, then snaps into place. The whole state machine is peer plus peer-checked: on a native checkbox, and the only thing utilities cannot express — the SVG goo filter and its stretch keyframe — stays in the stylesheet where it belongs.
## HTML
```html
<section class="lgt-04 grid w-full min-h-svh place-items-center p-15 font-lgt04-sans [background:radial-gradient(circle_at_30%_20%,#2a2740,#12101d)]" aria-label="Tailwind liquid gooey toggle switch">
<svg width="0" height="0" aria-hidden="true" focusable="false" class="absolute">
<filter id="lgt-04-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 20 -9" result="goo"/>
<feBlend in="SourceGraphic" in2="goo"/>
</filter>
</svg>
<label class="inline-flex cursor-pointer items-center gap-3.5 text-[22px] text-lgt04-fg">
<input type="checkbox" checked class="peer sr-only">
<span aria-hidden="true" class="relative h-[1.7em] w-[3.4em] rounded-full border border-white/25 bg-lgt04-off backdrop-blur-[6px] transition-colors duration-350 shadow-[inset_0_2px_8px_rgba(0,0,0,.45),inset_0_1px_0_rgba(255,255,255,.25)] peer-checked:bg-lgt04-on peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-white peer-checked:[&_.lgt-04-fixed]:opacity-0 peer-checked:[&_.lgt-04-handle]:left-[2em] peer-checked:[&_.lgt-04-handle]:animate-lgt04-stretch motion-reduce:[&_.lgt-04-handle]:animate-none">
<span class="absolute inset-0 [filter:url(#lgt-04-goo)]">
<span class="lgt-04-fixed absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white opacity-90 transition-opacity delay-200 duration-300"></span>
<span class="lgt-04-handle absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white shadow-[0_2px_6px_rgba(0,0,0,.35)] transition-[left] duration-400 ease-lgt04"></span>
</span>
</span>
<span class="text-[0.85em] font-semibold">Liquid mode</span>
</label>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-lgt04-on: #34d3a4;
--color-lgt04-on: oklch(0.7 0.17 165);
--color-lgt04-off: #6c6a85;
--color-lgt04-off: oklch(0.55 0.03 260);
--color-lgt04-fg: #eceaf5;
--font-lgt04-sans: 'Manrope', system-ui, sans-serif;
--ease-lgt04: cubic-bezier(0.6, -0.4, 0.4, 1.4);
--animate-lgt04-stretch: lgt-04-stretch 0.4s ease;
}
/* The two things utilities cannot express: a keyframe, and the -50% Y that
must survive inside the scaleX so the handle stays vertically centred. */
@keyframes lgt-04-stretch {
0% {
transform: translateY(-50%) scaleX(1);
}
45% {
transform: translateY(-50%) scaleX(1.55);
}
100% {
transform: translateY(-50%) scaleX(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 Liquid Gooey Toggle Switch
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-gooey-toggle-switch/
A handle that behaves like liquid metal: it stretches as it travels, fuses with the track blob, then snaps into place. The whole state machine is peer plus peer-checked: on a native checkbox, and the only thing utilities cannot express — the SVG goo filter and its stretch keyframe — stays in the stylesheet where it belongs.
## HTML
```html
<section class="lgt-04 grid w-full min-h-svh place-items-center p-15 font-lgt04-sans [background:radial-gradient(circle_at_30%_20%,#2a2740,#12101d)]" aria-label="Tailwind liquid gooey toggle switch">
<svg width="0" height="0" aria-hidden="true" focusable="false" class="absolute">
<filter id="lgt-04-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 20 -9" result="goo"/>
<feBlend in="SourceGraphic" in2="goo"/>
</filter>
</svg>
<label class="inline-flex cursor-pointer items-center gap-3.5 text-[22px] text-lgt04-fg">
<input type="checkbox" checked class="peer sr-only">
<span aria-hidden="true" class="relative h-[1.7em] w-[3.4em] rounded-full border border-white/25 bg-lgt04-off backdrop-blur-[6px] transition-colors duration-350 shadow-[inset_0_2px_8px_rgba(0,0,0,.45),inset_0_1px_0_rgba(255,255,255,.25)] peer-checked:bg-lgt04-on peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-white peer-checked:[&_.lgt-04-fixed]:opacity-0 peer-checked:[&_.lgt-04-handle]:left-[2em] peer-checked:[&_.lgt-04-handle]:animate-lgt04-stretch motion-reduce:[&_.lgt-04-handle]:animate-none">
<span class="absolute inset-0 [filter:url(#lgt-04-goo)]">
<span class="lgt-04-fixed absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white opacity-90 transition-opacity delay-200 duration-300"></span>
<span class="lgt-04-handle absolute top-1/2 left-[0.28em] size-[1.14em] -translate-y-1/2 rounded-full bg-white shadow-[0_2px_6px_rgba(0,0,0,.35)] transition-[left] duration-400 ease-lgt04"></span>
</span>
</span>
<span class="text-[0.85em] font-semibold">Liquid mode</span>
</label>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-lgt04-on: #34d3a4;
--color-lgt04-on: oklch(0.7 0.17 165);
--color-lgt04-off: #6c6a85;
--color-lgt04-off: oklch(0.55 0.03 260);
--color-lgt04-fg: #eceaf5;
--font-lgt04-sans: 'Manrope', system-ui, sans-serif;
--ease-lgt04: cubic-bezier(0.6, -0.4, 0.4, 1.4);
--animate-lgt04-stretch: lgt-04-stretch 0.4s ease;
}
/* The two things utilities cannot express: a keyframe, and the -50% Y that
must survive inside the scaleX so the handle stays vertically centred. */
@keyframes lgt-04-stretch {
0% {
transform: translateY(-50%) scaleX(1);
}
45% {
transform: translateY(-50%) scaleX(1.55);
}
100% {
transform: translateY(-50%) scaleX(1);
}
}
```How this works
The checkbox is the component. Not a div with role=switch, not a JS store — a native input, clipped with sr-only so it stays focusable, announceable and Space-togglable. Everything visible is a sibling that reads its state:
<input type="checkbox" class="peer sr-only">
<span class="bg-lgt04-off peer-checked:bg-lgt04-on
peer-focus-visible:outline-3 peer-focus-visible:outline-white">Reaching inside a sibling needs the child escape. peer-checked: styles the sibling itself; the handle is two levels down inside it. peer-checked:[&_.lgt-04-handle]:left-8 is the idiomatic answer, and forgetting it is the single most common bug when porting a :checked + .track .handle rule.
The goo is SVG, and it stays SVG. feGaussianBlur blurs the blobs, then feColorMatrix hard-thresholds the alpha, so overlapping shapes fuse into one organic mass. There is no CSS equivalent and no utility for it — the filter lives in the markup, and one arbitrary [filter:url(#...)] utility points at it.
The stretch is a theme animation. Declaring --animate-lgt04-stretch in @theme turns the whole shorthand into a utility, so the markup reads peer-checked:[&_.lgt-04-handle]:animate-lgt04-stretch and the timing lives in one place.
Make it yours
- Change how liquid it feels by editing
stdDeviationon the blur (higher = more viscous) and the alpha row of thefeColorMatrix(higher multiplier = harder edge). - Recolour the two states from
@theme:--color-lgt04-onand--color-lgt04-offare the only colour decisions in the demo. - Resize the whole switch from the root
text-[22px]— every dimension is em-based, so one number scales it. - Tune the stretch amount in the
@keyframesscaleX values; 1.55 is the point where it reads as liquid rather than as a glitch. - Slow the travel with
duration-500on the handle for a heavier, more mercurial feel.
Gotchas — read before shipping
- The goo filter must wrap the blob layer only. Applying it to the switch root smears the label text into an unreadable blur.
- Use
sr-only, neverhiddenordisplay:none, on the input — a hidden checkbox is unreachable by keyboard and invisible to screen readers. peer-*matches previous siblings only. If the input comes after the track in source order, nothing works and there is no error to tell you why.- SVG filter ids are document-global. Two un-namespaced
#goofilters on one page silently both resolve to the first, which is why every id here carries thelgt-04-prefix. - The blob layer needs a real backdrop; goo over a fully transparent parent renders as hard-edged circles.
motion-reduce:animate-noneis required here — a stretching blob is exactly the kind of organic motion that triggers vestibular discomfort.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Utilities only, no JavaScript. SVG filters and :checked are universally supported; Tailwind v4's generated sheet sets the practical floor. Under prefers-reduced-motion the stretch drops to a plain slide.