51 CSS Buttons31 / 51

Pure CSSMIT licensed

CSS Mercury Bead Morph Button

A bead of mercury detaches from the button, stretches, and is reabsorbed on hover — genuine metaball merging, produced by an SVG goo filter (blur, then a hard alpha threshold) applied to a layer of plain CSS shapes. No canvas, no WebGL, no JavaScript.

Published Updated

Live Demo
Try it

The code

<div class="cb-31">
  <div class="cb-31__stage">
    <svg class="cb-31__defs" aria-hidden="true" focusable="false" width="0" height="0">
      <filter id="cb-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 class="cb-31__btn" type="button">
      <span class="cb-31__goo" aria-hidden="true">
        <span class="cb-31__pill"></span>
        <span class="cb-31__bead"></span>
      </span>
      <span class="cb-31__label">Liquify</span>
    </button>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap');

.cb-31 {
  --bg: #0c0e12;
  --hg: #c9d2dc;
  --hg-hi: #ffffff;
  --hg-lo: #5c6672;
  --ink: #0b0d11;
  --sans: "Space Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--bg);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@media (prefers-color-scheme: light) {
  .cb-31:not([data-theme="dark"]) {
    --bg: #e7eaee;
  }
}

.cb-31[data-theme="light"] {
  --bg: #e7eaee;
}

.cb-31,
.cb-31 *,
.cb-31 *::before,
.cb-31 *::after {
  box-sizing: border-box;
}

.cb-31__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-31__defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.cb-31__btn {
  position: relative;
  cursor: pointer;
  border: 0;
  background: transparent;
  width: 16rem;
  height: 8rem;
  padding: 0;
  font: inherit;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}

.cb-31__label {
  position: absolute;
  z-index: 3;
  left: 3.9rem;
  top: 50%;
  translate: 0 -50%;
  transition: translate .5s cubic-bezier(.16,1,.3,1), color .4s ease;
}

.cb-31__goo {
  position: absolute;
  inset: 0;
  z-index: 1;
  filter: url(#cb-31-goo);
}
/* 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. */

.cb-31__pill,
.cb-31__bead {
  position: absolute;
  border-radius: 999px;
  background-image: radial-gradient(120% 120% at 30% 20%, var(--hg-hi), transparent 55%), linear-gradient(160deg, var(--hg), var(--hg-lo));
  background-size: 8.2rem 3.2rem, 8.2rem 3.2rem;
  background-position: 0 0, 0 0;
  background-repeat: no-repeat;
}

.cb-31__pill {
  left: 2.2rem;
  top: 2.4rem;
  width: 8.2rem;
  height: 3.2rem;
}

.cb-31__bead {
  left: 2.2rem;
  top: 2.4rem;
  width: 3.2rem;
  height: 3.2rem;
  transition: translate .75s cubic-bezier(.16,1,.3,1), scale .75s cubic-bezier(.16,1,.3,1);
}

.cb-31__btn:is(:hover,:focus-visible) .cb-31__bead {
  translate: 8.4rem 0;
  scale: .62;
}

.cb-31__btn:is(:hover,:focus-visible) .cb-31__label {
  translate: .4rem -50%;
}

.cb-31__btn:focus-visible {
  outline: 2px solid var(--hg);
  outline-offset: 2px;
  border-radius: 1rem;
}

.cb-31__btn:active .cb-31__bead {
  scale: .5;
}

.cb-31__btn:disabled {
  cursor: not-allowed;
  filter: grayscale(1) brightness(.8);
}

@media (prefers-reduced-motion: reduce) {
  .cb-31__bead,
    .cb-31__label {
    transition: none;
  }
}

@media (forced-colors: active) {
  .cb-31__goo {
    display: none;
  }

  .cb-31__btn {
    border: 1px solid ButtonText;
    background: ButtonFace;
    width: auto;
    height: auto;
    padding: 1rem 2rem;
  }

  .cb-31__label {
    position: static;
    translate: none;
    color: ButtonText;
  }
}
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 Button 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: CSS Mercury Bead Morph Button
Source: https://codefronts.com/components/css-buttons/css-mercury-bead-morph-button/

A bead of mercury detaches from the button, stretches, and is reabsorbed on hover — genuine metaball merging, produced by an SVG goo filter (blur, then a hard alpha threshold) applied to a layer of plain CSS shapes. No canvas, no WebGL, no JavaScript.
## HTML
```html
<div class="cb-31">
  <div class="cb-31__stage">
    <svg class="cb-31__defs" aria-hidden="true" focusable="false" width="0" height="0">
      <filter id="cb-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 class="cb-31__btn" type="button">
      <span class="cb-31__goo" aria-hidden="true">
        <span class="cb-31__pill"></span>
        <span class="cb-31__bead"></span>
      </span>
      <span class="cb-31__label">Liquify</span>
    </button>
  </div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap');

.cb-31 {
  --bg: #0c0e12;
  --hg: #c9d2dc;
  --hg-hi: #ffffff;
  --hg-lo: #5c6672;
  --ink: #0b0d11;
  --sans: "Space Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: radial-gradient(36rem 22rem at 50% 45%, #1b2029, transparent 72%), var(--bg);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@media (prefers-color-scheme: light) {
  .cb-31:not([data-theme="dark"]) {
    --bg: #e7eaee;
  }
}

.cb-31[data-theme="light"] {
  --bg: #e7eaee;
}

.cb-31,
.cb-31 *,
.cb-31 *::before,
.cb-31 *::after {
  box-sizing: border-box;
}

.cb-31__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-31__defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.cb-31__btn {
  position: relative;
  cursor: pointer;
  border: 0;
  background: transparent;
  width: 16rem;
  height: 8rem;
  padding: 0;
  font: inherit;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}

.cb-31__label {
  position: absolute;
  z-index: 3;
  left: 3.9rem;
  top: 50%;
  translate: 0 -50%;
  transition: translate .5s cubic-bezier(.16,1,.3,1), color .4s ease;
}

.cb-31__goo {
  position: absolute;
  inset: 0;
  z-index: 1;
  filter: url(#cb-31-goo);
}
/* 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. */

.cb-31__pill,
.cb-31__bead {
  position: absolute;
  border-radius: 999px;
  background-image: radial-gradient(120% 120% at 30% 20%, var(--hg-hi), transparent 55%), linear-gradient(160deg, var(--hg), var(--hg-lo));
  background-size: 8.2rem 3.2rem, 8.2rem 3.2rem;
  background-position: 0 0, 0 0;
  background-repeat: no-repeat;
}

.cb-31__pill {
  left: 2.2rem;
  top: 2.4rem;
  width: 8.2rem;
  height: 3.2rem;
}

.cb-31__bead {
  left: 2.2rem;
  top: 2.4rem;
  width: 3.2rem;
  height: 3.2rem;
  transition: translate .75s cubic-bezier(.16,1,.3,1), scale .75s cubic-bezier(.16,1,.3,1);
}

.cb-31__btn:is(:hover,:focus-visible) .cb-31__bead {
  translate: 8.4rem 0;
  scale: .62;
}

.cb-31__btn:is(:hover,:focus-visible) .cb-31__label {
  translate: .4rem -50%;
}

.cb-31__btn:focus-visible {
  outline: 2px solid var(--hg);
  outline-offset: 2px;
  border-radius: 1rem;
}

.cb-31__btn:active .cb-31__bead {
  scale: .5;
}

.cb-31__btn:disabled {
  cursor: not-allowed;
  filter: grayscale(1) brightness(.8);
}

@media (prefers-reduced-motion: reduce) {
  .cb-31__bead,
    .cb-31__label {
    transition: none;
  }
}

@media (forced-colors: active) {
  .cb-31__goo {
    display: none;
  }

  .cb-31__btn {
    border: 1px solid ButtonText;
    background: ButtonFace;
    width: auto;
    height: auto;
    padding: 1rem 2rem;
  }

  .cb-31__label {
    position: static;
    translate: none;
    color: ButtonText;
  }
}
```

How this works

The goo is two SVG primitives: blur everything together, then push the alpha channel through a steep curve so overlapping blurs snap into one continuous shape:

<filter id="cb-31-goo">
  <feGaussianBlur in="SourceGraphic" stdDeviation="7" 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="g"/>
  <feBlend in="SourceGraphic" in2="g"/>
</filter>

The last row of the matrix is the whole trick: multiply alpha by 20 and subtract 9, and a soft blur becomes a hard edge. The filter goes on a wrapper containing the pill and the bead — never on the text:

.cb-31__goo{ filter:url(#cb-31-goo); }
/* rest: the bead exactly covers the pill's cap, so it is invisible */
.cb-31__bead{ left:2.2rem; top:2.4rem; width:3.2rem; height:3.2rem; }
.cb-31__btn:hover .cb-31__bead{ translate:8.4rem 0; scale:.62; }

Two details make the illusion hold: the bead matches the pill's cap exactly at rest, and both elements paint the identical gradient at the identical background-size, so there is no tonal seam to give the hidden lump away. On hover it shrinks as it travels — a droplet pinching off rather than a ball sliding out. The label sits outside the filtered layer at a higher z-index, so it stays perfectly sharp while the metal behind it flows. A chrome highlight gradient on both shapes sells the liquid metal.

Make it yours

  • Bead travel and size live in the hover rule: a shorter throw with a larger scale reads as thicker liquid, a longer throw with a smaller one as a thin drip.
  • Raise stdDeviation for a gloopier merge — but the alpha multiplier must rise with it or edges go soft.
  • Recolour with --hg; keep the highlight near-white or it stops looking metallic.
  • Add a second bead with a delay for a chain of droplets.

Gotchas — read before shipping

  • Never apply the goo filter to an element containing text — feGaussianBlur blurs glyphs and no amount of thresholding brings them back.
  • SVG filters force a software raster pass on some GPUs; one instance is fine, twenty in a list is not.
  • The filter's id must be unique per page. Namespaced ids (cb-31-goo) are why every demo in this collection is numerically scoped.
  • filter creates a containing block and clips at the filter region — give the wrapper generous padding or the bead gets cut off mid-flight.
  • Firefox and Safari rasterise filter regions at different resolutions; verify the threshold values in both, and keep stdDeviation modest.
  • A bead that is supposed to be hidden at rest must match the shape it hides inside — same size AND the same painted gradient (background-size and background-position, not merely the same colour stops). The goo threshold merges silhouettes; it does not blend tones, so a lighter bead shows through as a disc behind your label.

Browser support

ChromeSafariFirefoxEdge
88+14+78+88+

SVG filters via filter:url() are supported in all modern engines. Rendering fidelity of feColorMatrix thresholds varies slightly between Blink, WebKit and Gecko — test the exact values.

Techniques used in this demo

Search CodeFronts

Loading…