WebberUI

Gooey Menu

A liquid, gooey expanding menu — click the floating trigger and the items are "pulled" out of the button through an SVG goo filter and fuse together, with four directions plus radial fanning.

Loading preview…
npx shadcn@latest add https://webberui.com/r/gooey-menu.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

16
44
56
<GooeyMenu />

Installation

npx shadcn@latest add https://webberui.com/r/gooey-menu.json

Or, once registries are configured in components.json, install it as @webberui/gooey-menu.

Usage

import { Heart, Bookmark, Share2 } from "lucide-react";
import { GooeyMenu, type GooeyMenuItem } from "@/components/ui/gooey-menu";

const items: GooeyMenuItem[] = [
  { icon: <Heart />, label: "Like", onClick: () => like() },
  { icon: <Bookmark />, label: "Save", onClick: () => save() },
  { icon: <Share2 />, label: "Share", href: "/share" },
];

<GooeyMenu items={items} direction="up" />;

controlled mode:

const [open, setOpen] = useState(false);

<GooeyMenu items={items} open={open} onOpenChange={setOpen} direction="radial" />;

Props

PropTypeDefaultDescription
itemsGooeyMenuItem[]Menu items (see the table below)
direction'up' | 'down' | 'left' | 'right' | 'radial''up'Expansion direction; radial fans upward
openbooleanControlled open state; when omitted, the component manages it internally
onOpenChange(open: boolean) => voidCallback fired when the open state changes
iconReactNode<Plus />Trigger icon; the default icon rotates 45° into a cross when open
labelstring"Open/close menu"Accessible label for the trigger
gapnumber16Spacing between droplets (px)
itemSizenumber44Item diameter (px)
triggerSizenumber56Trigger diameter (px)
radiusnumberderived automaticallyFan radius for radial mode (px)
closeOnSelectbooleantrueWhether clicking an item collapses the menu automatically
classNamestringAppended to the root container's className

GooeyMenuItem

FieldTypeDescription
iconReactNodeIcon (lucide-react recommended; the size is controlled by the component)
labelstringAccessible label, also used as the hint text on hover / focus
onClick() => voidClick callback
hrefstringWhen provided, this item renders as an <a> link
classNamestringAppended to this item's button className

How it works

  • The goo filter: the liquid layer gets an SVG feGaussianBlur + feColorMatrix — blur first, then push up the alpha contrast — which fuses the round backgrounds of the trigger and the items into one organic, gooey surface; the filter id comes from useId, so several menus on the same page never contaminate each other
  • Two layers: the solid-color droplet backgrounds and the icons live on separate layers — the background layer handles the gooey fusing, while the icons and click interactions sit on an upper layer with no filter, so the icons always stay sharp and untouched by the blur
  • Gooey pull: when collapsed, all the items shrink back into the trigger's center and hide inside a droplet of the same color; expanding staggers them near-to-far and collapsing far-to-near, and the spring makes the droplets look "pulled" out and "sucked" back
  • Direction and fanning: up / down / left / right lay the items out evenly along a single axis, while radial fans them out centered on straight up (the more items, the wider the fan)

Accessibility

  • The trigger is a native <button> carrying aria-haspopup="menu", aria-expanded, and aria-controls; the item container is role="menu" and each item is role="menuitem"
  • Full keyboard support: pressing an arrow key on the trigger opens the menu and focuses the first item; items are navigated with a roving tabindex plus the arrow keys/Home/End; Esc collapses the menu and focus returns to the trigger
  • Clicking outside the menu collapses it automatically; every item is named with an aria-label, and hovering or focusing additionally shows a visual hint label (the label itself is aria-hidden to avoid being read twice)
  • The liquid layer and the filter are both marked aria-hidden — decorative only, so they do not interfere with assistive technology
  • When the user has "reduce motion" enabled at the system level, the staggered droplet timing along with the press and hover scaling are all disabled, switching to instant open and close (with the DOM structure unchanged)

On this page