47 CSS Toggles & Switches04 / 47

Pure CSSMIT licensed

Gooey Liquid Morphing Switch

A high-impact micro-interaction: the thumb is a blob of liquid that stretches, necks and snaps to the far side of the track like plasma — built with an inline SVG filter (feGaussianBlur + feColorMatrix) and two staggered CSS transitions. No JavaScript, no canvas.

Published

Live Demo
Try it

The code

<section class="tgl-04" aria-label="Gooey liquid morphing switch">
  <svg width="0" height="0" aria-hidden="true" style="position:absolute">
    <defs><filter id="tgl-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 18 -7" result="g"/><feComposite in="SourceGraphic" in2="g" operator="atop"/></filter></defs>
  </svg>
  <label class="tgl-04__row" for="tgl-04-sw">
    <span class="tgl-04__lbl">Liquid mode</span>
    <span class="tgl-04__track">
      <span class="tgl-04__goo" aria-hidden="true"><i class="tgl-04__blob"></i><i class="tgl-04__echo"></i></span>
      <input type="checkbox" id="tgl-04-sw" class="tgl-04__sw" role="switch">
    </span>
  </label>
  <p class="tgl-04__cap">Watch the drop neck &amp; snap</p>
</section>
.tgl-04,
.tgl-04 *,
.tgl-04 *::before,
.tgl-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tgl-04 {
  --goo: oklch(0.72 0.19 340);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 60px 20px;
  background: #171222;
}

.tgl-04__row {
  display: flex;
  align-items: center;
  gap: 30px;
  cursor: pointer;
}

.tgl-04__lbl {
  font-size: 16px;
  font-weight: 600;
  color: #cfc6e4;
}

.tgl-04__track {
  position: relative;
  width: 96px;
  height: 48px;
  border-radius: 26px;
  background: #241b38;
  box-shadow: inset 0 3px 8px rgba(0,0,0,.55);
}

.tgl-04__goo {
  position: absolute;
  inset: 0;
  filter: url(#tgl-04-goo);
  pointer-events: none;
}

.tgl-04__blob,
.tgl-04__echo {
  position: absolute;
  top: 8px;
  left: 8px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--goo);
}

.tgl-04__blob {
  transition: transform .5s cubic-bezier(.65,0,.35,1);
}

.tgl-04__echo {
  transition: transform .5s cubic-bezier(.65,0,.35,1) .09s;
  transform: scale(.86);
}

.tgl-04__sw {
  appearance: none;
  -webkit-appearance: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 26px;
  cursor: pointer;
  background: transparent;
}

.tgl-04__sw:checked ~ * .tgl-04__blob {
}

.tgl-04__track:has(.tgl-04__sw:checked) .tgl-04__blob {
  transform: translateX(48px);
}

.tgl-04__track:has(.tgl-04__sw:checked) .tgl-04__echo {
  transform: translateX(48px) scale(.86);
}

.tgl-04__sw:focus-visible {
  outline: 3px solid var(--goo);
  outline-offset: 4px;
}

.tgl-04__cap {
  font-size: 12px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #6f6588;
}

@media (prefers-reduced-motion: reduce) {
  .tgl-04__blob,
    .tgl-04__echo {
    transition: transform .2s;
  }

  .tgl-04__echo {
    transition-delay: 0s;
  }
}
/* No JavaScript — an SVG feGaussianBlur + feColorMatrix filter fuses a thumb blob with a delayed echo blob, drawing a liquid neck as they separate mid-slide. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Toggles & Switche 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: Gooey Liquid Morphing Switch
Source: https://codefronts.com/components/css-toggles-switches/gooey-liquid-morphing-switch/

A high-impact micro-interaction: the thumb is a blob of liquid that stretches, necks and snaps to the far side of the track like plasma — built with an inline SVG filter (feGaussianBlur + feColorMatrix) and two staggered CSS transitions. No JavaScript, no canvas.
## HTML
```html
<section class="tgl-04" aria-label="Gooey liquid morphing switch">
  <svg width="0" height="0" aria-hidden="true" style="position:absolute">
    <defs><filter id="tgl-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 18 -7" result="g"/><feComposite in="SourceGraphic" in2="g" operator="atop"/></filter></defs>
  </svg>
  <label class="tgl-04__row" for="tgl-04-sw">
    <span class="tgl-04__lbl">Liquid mode</span>
    <span class="tgl-04__track">
      <span class="tgl-04__goo" aria-hidden="true"><i class="tgl-04__blob"></i><i class="tgl-04__echo"></i></span>
      <input type="checkbox" id="tgl-04-sw" class="tgl-04__sw" role="switch">
    </span>
  </label>
  <p class="tgl-04__cap">Watch the drop neck &amp; snap</p>
</section>
```
## CSS
```css
.tgl-04,
.tgl-04 *,
.tgl-04 *::before,
.tgl-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tgl-04 {
  --goo: oklch(0.72 0.19 340);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 60px 20px;
  background: #171222;
}

.tgl-04__row {
  display: flex;
  align-items: center;
  gap: 30px;
  cursor: pointer;
}

.tgl-04__lbl {
  font-size: 16px;
  font-weight: 600;
  color: #cfc6e4;
}

.tgl-04__track {
  position: relative;
  width: 96px;
  height: 48px;
  border-radius: 26px;
  background: #241b38;
  box-shadow: inset 0 3px 8px rgba(0,0,0,.55);
}

.tgl-04__goo {
  position: absolute;
  inset: 0;
  filter: url(#tgl-04-goo);
  pointer-events: none;
}

.tgl-04__blob,
.tgl-04__echo {
  position: absolute;
  top: 8px;
  left: 8px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--goo);
}

.tgl-04__blob {
  transition: transform .5s cubic-bezier(.65,0,.35,1);
}

.tgl-04__echo {
  transition: transform .5s cubic-bezier(.65,0,.35,1) .09s;
  transform: scale(.86);
}

.tgl-04__sw {
  appearance: none;
  -webkit-appearance: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 26px;
  cursor: pointer;
  background: transparent;
}

.tgl-04__sw:checked ~ * .tgl-04__blob {
}

.tgl-04__track:has(.tgl-04__sw:checked) .tgl-04__blob {
  transform: translateX(48px);
}

.tgl-04__track:has(.tgl-04__sw:checked) .tgl-04__echo {
  transform: translateX(48px) scale(.86);
}

.tgl-04__sw:focus-visible {
  outline: 3px solid var(--goo);
  outline-offset: 4px;
}

.tgl-04__cap {
  font-size: 12px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #6f6588;
}

@media (prefers-reduced-motion: reduce) {
  .tgl-04__blob,
    .tgl-04__echo {
    transition: transform .2s;
  }

  .tgl-04__echo {
    transition-delay: 0s;
  }
}
```

## JavaScript
```js
/* No JavaScript — an SVG feGaussianBlur + feColorMatrix filter fuses a thumb blob with a delayed echo blob, drawing a liquid neck as they separate mid-slide. */
```

How this works

The classic gooey recipe: a container is filtered through filter:url(#goo), whose SVG chain blurs everything inside (feGaussianBlur) then slams the alpha channel's contrast back up (feColorMatrix 18 / −7). Any two shapes that overlap while blurred fuse into one liquid silhouette.

Inside the filtered layer live two blobs: the main thumb and a trailing echo with a slightly longer, delayed transition. Mid-travel they separate just enough for the filter to draw a stretching neck between them; on arrival the echo catches up and the drop snaps whole. The real checkbox sits above the goo, transparent but fully clickable and focusable.

Make it yours

  • More viscosity: raise the echo blob's transition-delay (try .12s).
  • Runnier goo: raise stdDeviation to 8 and rebalance the color matrix (20 / −9).
  • Recolor the liquid via --goo — the filter is color-agnostic.
  • Make it droopy: give the echo a few px of translateY at rest.

Gotchas — read before shipping

  • The filter must wrap ONLY the blobs — filtering the whole switch blurs the track and label too.
  • Keep blob edges ≥ blur radius away from the filtered container's edge or the goo clips flat.
  • Each demo instance needs a unique SVG filter id — duplicate ids silently break the second instance.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 53+.

SVG filters via CSS filter:url() are supported across modern browsers; Safari renders goo correctly from 9.1+. Degrades to a plain sliding thumb if filters fail.

Techniques used in this demo

Search CodeFronts

Loading…