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.
| Category | Effects |
|---|---|
| Type | CSS only (.mv-gooey) |
| Status | beta |
| Keywords | gooey, metaball, liquid, svg-filter, menu, fab, segmented, switch, blob |
When to use
- A floating action button should burst into actions that separate like liquid droplets
- A segmented switch should get a playful thumb that stretches and trails a droplet
- Custom blob shapes need to merge and split like metaballs behind crisp text and icons
Avoid when
- The menu has many actions or needs submenus, checkable items or typeahead → use Menu instead
Install
node scripts/add.mjs gooey --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
data-strength | sm | lg | (medium) | Filter viscosity (merge radius). |
data-open | boolean | On .mv-gooey-menu: menu expanded (managed by the script). |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-gooey | Container (stacking context). Variants data-strength="sm | lg". |
mv-gooey-layer | aria-hidden background layer that receives the filter; put the shapes in it. |
mv-gooey-blob | Solid shape (color --mv-gooey-color, pill by default) to position freely. |
mv-gooey-menu | Action 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-switch | Segmented 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
| Name | Default | Description |
|---|---|---|
--mv-gooey-color | var(--mv-accent) | Shape color. |
--mv-gooey-menu-size / --mv-gooey-menu-item | 3.5rem / 2.9rem | Size of the main button and of the actions. |
--mv-gooey-menu-radius | 7rem | Distance of the expanded actions. |
--mv-gooey-menu-start / --mv-gooey-menu-step | 180deg / 30deg | Expansion arc (180deg = to the left, 270deg = upward). |
--mv-gooey-switch-count | 3 | Number of switch options. |
--mv-gooey-switch-pad | 0.3rem | Track 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.