beta

Gooey — .mv-gooey

Metaball utility: a filtered background layer (blur + SVG alpha threshold) where shapes melt into each other like liquid, while text, icons and focus stay crisp on top. Ships with two ready-made patterns: an action menu that bursts out as droplets and a segmented switch with a trailing droplet.

CategoryEffects
TypeCSS only (.mv-gooey)
Statusbeta
Keywordsgooey, metaball, liquid, svg-filter, menu, fab, segmented, switch, blob

When to use

Avoid when

Install

node scripts/add.mjs gooey --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["gooey"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, components/gooey/gooey.css, components/gooey/gooey.js.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div style="display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:center;gap:3rem 4rem;width:100%">
  <div style="display:grid;gap:1.25rem;justify-items:start">
    <fieldset class="mv-gooey mv-gooey-switch" style="--mv-gooey-switch-count:3;width:19rem">
      <legend class="mv-sr-only">Time range</legend>
      <span class="mv-gooey-layer" aria-hidden="true"><span class="mv-gooey-blob mv-gooey-switch-drop"></span><span class="mv-gooey-blob mv-gooey-switch-thumb"></span></span>
      <label><input type="radio" name="mv-gooey-demo-period" value="day" checked>Day</label>
      <label><input type="radio" name="mv-gooey-demo-period" value="week">Week</label>
      <label><input type="radio" name="mv-gooey-demo-period" value="month">Month</label>
    </fieldset>
    <fieldset class="mv-gooey mv-gooey-switch" data-strength="lg" style="--mv-gooey-switch-count:2;--mv-gooey-color:var(--mv-success);width:12rem">
      <legend class="mv-sr-only">Status</legend>
      <span class="mv-gooey-layer" aria-hidden="true"><span class="mv-gooey-blob mv-gooey-switch-drop"></span><span class="mv-gooey-blob mv-gooey-switch-thumb"></span></span>
      <label><input type="radio" name="mv-gooey-demo-status" value="online" checked>Online</label>
      <label><input type="radio" name="mv-gooey-demo-status" value="away">Away</label>
    </fieldset>
    <p style="margin:0;font-size:.8rem;color:var(--mv-fg-subtle);max-width:19rem">The filter only affects the background shapes, so text stays crisp.</p>
  </div>

  <div class="mv-gooey mv-gooey-menu" id="mv-gooey-demo-menu">
    <span class="mv-gooey-layer" aria-hidden="true">
      <span class="mv-gooey-blob" style="--i:0"></span>
      <span class="mv-gooey-blob" style="--i:1"></span>
      <span class="mv-gooey-blob" style="--i:2"></span>
      <span class="mv-gooey-blob" style="--i:3"></span>
      <span class="mv-gooey-blob mv-gooey-menu-core"></span>
    </span>
    <div id="mv-gooey-demo-items">
      <button type="button" class="mv-gooey-menu-item" style="--i:0" aria-label="New note"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></button>
      <button type="button" class="mv-gooey-menu-item" style="--i:1" aria-label="Photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.5"/></svg></button>
      <button type="button" class="mv-gooey-menu-item" style="--i:2" aria-label="Reminder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.9 1.9 0 0 0 3.4 0"/></svg></button>
      <button type="button" class="mv-gooey-menu-item" style="--i:3" aria-label="Share"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/></svg></button>
    </div>
    <button type="button" class="mv-gooey-menu-trigger" aria-expanded="false" aria-controls="mv-gooey-demo-items" aria-label="Create"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
  </div>
</div>

API

Attributes

NameTypeDefaultDescription
data-strengthsm | lg(medium)Filter viscosity (merge radius).
data-openbooleanOn .mv-gooey-menu: menu expanded (managed by the script).

Methods

NameDescription
ensureGooeyFilters()JS export: injects the SVG filters (#mv-gooey, -sm, -lg); called automatically on load.
setGooeyMenu(menu, open)JS export: opens or closes a .mv-gooey-menu (aria-expanded, data-open, item tabindex).

CSS classes

NameDescription
mv-gooeyContainer (stacking context). Variants data-strength="sm | lg".
mv-gooey-layeraria-hidden background layer that receives the filter; put the shapes in it.
mv-gooey-blobSolid shape (color --mv-gooey-color, pill by default) to position freely.
mv-gooey-menuAction menu pattern: .mv-gooey-menu-trigger (aria-expanded, aria-controls), .mv-gooey-menu-item elements with --i, and one .mv-gooey-blob per item (same --i) + .mv-gooey-menu-core. Opened with data-open.
mv-gooey-switchSegmented switch pattern on a <fieldset> of radios: layer with .mv-gooey-switch-thumb and .mv-gooey-switch-drop (up to 6 options, via :has).

CSS variables

NameDefaultDescription
--mv-gooey-colorvar(--mv-accent)Shape color.
--mv-gooey-menu-size / --mv-gooey-menu-item3.5rem / 2.9remSize of the main button and of the actions.
--mv-gooey-menu-radius7remDistance of the expanded actions.
--mv-gooey-menu-start / --mv-gooey-menu-step180deg / 30degExpansion arc (180deg = to the left, 270deg = upward).
--mv-gooey-switch-count3Number of switch options.
--mv-gooey-switch-pad0.3remTrack inner padding.

Accessibility

The filtered layer is aria-hidden and ignores events: the real controls (buttons, radios) sit on top, crisp and focusable. Menu: button with aria-expanded / aria-controls, actions removed from the tab order when closed, Escape closes and returns focus, outside click closes. Switch: native radios in a fieldset with a legend (native arrow keys). Reduced motion: transitions become instant (duration tokens). Without JS: the filter is missing and shapes stay plain pills.