Pure TailwindMIT licensed

Liquid Goo Tailwind Close Button (SVG Filter)

Metaballs in utilities: a blurred, re-thresholded SVG filter applied with [filter:url(#tcb15-goo)] fuses a main blob and two droplets into one liquid shape. Hover (or keyboard focus) flings the droplets out on a back-and-overshoot curve with group-hover/stack:translate-x-[38px], and the surface tension visibly snaps as they separate.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tcb-15 w-full min-h-svh grid place-items-center bg-[linear-gradient(170deg,var(--color-tcb15-bg),var(--color-tcb15-bg2))] px-6 py-12 font-tcb15-sans" aria-label="Liquid goo close button demo">
  <svg class="absolute" width="0" height="0" aria-hidden="true"><defs><filter id="tcb15-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>
  <div class="grid w-full justify-items-center gap-7">
    <div class="group/stack relative grid size-[130px] place-items-center">
      <div aria-hidden="true" class="absolute inset-0 grid place-items-center [filter:url(#tcb15-goo)]">
        <span class="absolute size-16 rounded-full bg-tcb15-goo"></span>
        <span class="absolute size-5 rounded-full bg-tcb15-goo transition-[translate] duration-550 ease-[cubic-bezier(.68,-.55,.265,1.55)] group-hover/stack:translate-x-[38px] group-hover/stack:-translate-y-[27px] group-has-[:focus-visible]/stack:translate-x-[38px] group-has-[:focus-visible]/stack:-translate-y-[27px] motion-reduce:transition-none motion-reduce:translate-0!"></span>
        <span class="absolute size-3.5 rounded-full bg-tcb15-goo transition-[translate] delay-40 duration-550 ease-[cubic-bezier(.68,-.55,.265,1.55)] group-hover/stack:-translate-x-[34px] group-hover/stack:translate-y-[30px] group-has-[:focus-visible]/stack:-translate-x-[34px] group-has-[:focus-visible]/stack:translate-y-[30px] motion-reduce:transition-none motion-reduce:translate-0!"></span>
      </div>
      <button type="button" aria-label="Close" class="relative grid size-16 cursor-pointer place-items-center rounded-full transition-[scale] duration-300 ease-[cubic-bezier(.34,1.56,.64,1)] hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-tcb15-ring active:scale-90 motion-reduce:transition-none">
        <span aria-hidden="true" class="relative size-5.5 before:absolute before:top-1/2 before:left-1/2 before:h-[3px] before:w-5.5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-white after:absolute after:top-1/2 after:left-1/2 after:h-[3px] after:w-5.5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-white"></span>
      </button>
    </div>
    <p aria-hidden="true" class="rounded-full bg-white/70 px-3.5 py-2 font-tcb15-mono text-[11.5px] text-tcb15-mut ring-1 ring-black/8">[filter:url(#tcb15-goo)] · group-hover/stack:translate-x-[38px]</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcb15-bg: #fbf1f5;
  --color-tcb15-bg: oklch(0.96 0.012 350);
  --color-tcb15-bg2: #f3dde6;
  --color-tcb15-bg2: oklch(0.92 0.03 340);
  --color-tcb15-goo: #dd3d87;
  --color-tcb15-goo: oklch(0.6 0.21 350);
  --color-tcb15-ring: #a8155b;
  --color-tcb15-ring: oklch(0.48 0.19 355);
  --color-tcb15-mut: #7a4c60;
  --color-tcb15-mut: oklch(0.47 0.07 350);
  --font-tcb15-sans: 'Geist', system-ui, sans-serif;
  --font-tcb15-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-15 {
  width: 100%;
  min-height: 100svh;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tcb15-bg: #fbf1f5;
  --color-tcb15-bg: oklch(0.96 0.012 350);
  --color-tcb15-bg2: #f3dde6;
  --color-tcb15-bg2: oklch(0.92 0.03 340);
  --color-tcb15-goo: #dd3d87;
  --color-tcb15-goo: oklch(0.6 0.21 350);
  --color-tcb15-ring: #a8155b;
  --color-tcb15-ring: oklch(0.48 0.19 355);
  --color-tcb15-mut: #7a4c60;
  --color-tcb15-mut: oklch(0.47 0.07 350);
  --font-tcb15-sans: 'Geist', system-ui, sans-serif;
  --font-tcb15-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-15 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • translate(calc(var(--fling) * 1), calc(var(--fling) * -0.7)) became literal translate-x-[38px] -translate-y-[27px] — the maths is done once, at authoring time.
  • UI upgrade from the CSS original: the goo darkened from oklch(0.65) to 0.6 so the white × clears 4.5:1 on it, and the focus ring moved to a deeper ink that separates from the pink blob.
  • UI upgrade: keyboard focus now flings the droplets too (group-has-[:focus-visible]/stack:), and reduced motion pins them with translate-0!.
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 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: Liquid Goo Tailwind Close Button (SVG Filter)
Source: https://codefronts.com/components/tailwind-css-close-buttons/liquid-pop/

Metaballs in utilities: a blurred, re-thresholded SVG filter applied with [filter:url(#tcb15-goo)] fuses a main blob and two droplets into one liquid shape. Hover (or keyboard focus) flings the droplets out on a back-and-overshoot curve with group-hover/stack:translate-x-[38px], and the surface tension visibly snaps as they separate.
## HTML
```html
<section class="tcb-15 w-full min-h-svh grid place-items-center bg-[linear-gradient(170deg,var(--color-tcb15-bg),var(--color-tcb15-bg2))] px-6 py-12 font-tcb15-sans" aria-label="Liquid goo close button demo">
  <svg class="absolute" width="0" height="0" aria-hidden="true"><defs><filter id="tcb15-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>
  <div class="grid w-full justify-items-center gap-7">
    <div class="group/stack relative grid size-[130px] place-items-center">
      <div aria-hidden="true" class="absolute inset-0 grid place-items-center [filter:url(#tcb15-goo)]">
        <span class="absolute size-16 rounded-full bg-tcb15-goo"></span>
        <span class="absolute size-5 rounded-full bg-tcb15-goo transition-[translate] duration-550 ease-[cubic-bezier(.68,-.55,.265,1.55)] group-hover/stack:translate-x-[38px] group-hover/stack:-translate-y-[27px] group-has-[:focus-visible]/stack:translate-x-[38px] group-has-[:focus-visible]/stack:-translate-y-[27px] motion-reduce:transition-none motion-reduce:translate-0!"></span>
        <span class="absolute size-3.5 rounded-full bg-tcb15-goo transition-[translate] delay-40 duration-550 ease-[cubic-bezier(.68,-.55,.265,1.55)] group-hover/stack:-translate-x-[34px] group-hover/stack:translate-y-[30px] group-has-[:focus-visible]/stack:-translate-x-[34px] group-has-[:focus-visible]/stack:translate-y-[30px] motion-reduce:transition-none motion-reduce:translate-0!"></span>
      </div>
      <button type="button" aria-label="Close" class="relative grid size-16 cursor-pointer place-items-center rounded-full transition-[scale] duration-300 ease-[cubic-bezier(.34,1.56,.64,1)] hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-tcb15-ring active:scale-90 motion-reduce:transition-none">
        <span aria-hidden="true" class="relative size-5.5 before:absolute before:top-1/2 before:left-1/2 before:h-[3px] before:w-5.5 before:-translate-1/2 before:rotate-45 before:rounded-full before:bg-white after:absolute after:top-1/2 after:left-1/2 after:h-[3px] after:w-5.5 after:-translate-1/2 after:-rotate-45 after:rounded-full after:bg-white"></span>
      </button>
    </div>
    <p aria-hidden="true" class="rounded-full bg-white/70 px-3.5 py-2 font-tcb15-mono text-[11.5px] text-tcb15-mut ring-1 ring-black/8">[filter:url(#tcb15-goo)] · group-hover/stack:translate-x-[38px]</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcb15-bg: #fbf1f5;
  --color-tcb15-bg: oklch(0.96 0.012 350);
  --color-tcb15-bg2: #f3dde6;
  --color-tcb15-bg2: oklch(0.92 0.03 340);
  --color-tcb15-goo: #dd3d87;
  --color-tcb15-goo: oklch(0.6 0.21 350);
  --color-tcb15-ring: #a8155b;
  --color-tcb15-ring: oklch(0.48 0.19 355);
  --color-tcb15-mut: #7a4c60;
  --color-tcb15-mut: oklch(0.47 0.07 350);
  --font-tcb15-sans: 'Geist', system-ui, sans-serif;
  --font-tcb15-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.tcb-15 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

Blur + threshold = liquid. The filter blurs everything, then multiplies alpha by 18 and subtracts 7: soft overlaps become solid, soft edges become crisp. Two blobs near each other therefore read as one stretched droplet.

<div class="absolute inset-0 grid place-items-center [filter:url(#tcb15-goo)]">
  <span class="size-4 … group-hover/stack:translate-x-[38px] group-hover/stack:-translate-y-[27px]">

The button sits on top, unfiltered, so its × stays pin-sharp while the goo moves underneath.

Make it yours

  • Stickier goo: raise stdDeviation to 8 and the matrix to 22 -9.
  • Farther fling: change the translate distances (the droplets are clipped by nothing, so the stage is the limit).
  • Colour: edit --color-tcb15-goo; the filter preserves it.

Gotchas — read before shipping

  • SVG filter ids are document-global — keep them namespaced (tcb15-goo) when several demos share a page.
  • Filters force a repaint every frame the droplets move; keep the filtered layer small.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

filter:url() on HTML is universal; the floor is Tailwind v4.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…