Components
Popover
shadcn's Popover with goo. The panel blooms out of a droplet, overshoots, and splashes all around.
Customize
GoopPopover wraps the same reka-ui Popover primitive as shadcn-vue's Popover, with the same API. Focus handling, dismissal, and positioning work the same. Swap the imports and you're done.
Installation
Nuxt
pnpm add @goopui/nuxt
export default defineNuxtConfig({
modules: ['@goopui/nuxt'],
})
The module auto-imports every GoopPopover* part, adds goop.css, and points Tailwind at the component classes. It needs Tailwind CSS v4 and the shadcn tokens (--popover, --popover-foreground, --primary, --foreground, --ring, --radius).
Vue
pnpm add @goopui/vue
@import "tailwindcss";
@import "@goopui/vue/goop.css";
Usage
<script setup lang="ts">
// Nuxt auto-imports these. In plain Vue, import them:
import {
GoopButton,
GoopPopover,
GoopPopoverContent,
GoopPopoverDescription,
GoopPopoverHeader,
GoopPopoverTitle,
GoopPopoverTrigger,
} from '@goopui/vue'
</script>
<template>
<GoopPopover>
<GoopPopoverTrigger as-child>
<GoopButton variant="outline">Open popover</GoopButton>
</GoopPopoverTrigger>
<GoopPopoverContent>
<GoopPopoverHeader>
<GoopPopoverTitle>Dimensions</GoopPopoverTitle>
<GoopPopoverDescription>Set the dimensions for the layer.</GoopPopoverDescription>
</GoopPopoverHeader>
</GoopPopoverContent>
</GoopPopover>
</template>
The parts are GoopPopover, GoopPopoverTrigger, GoopPopoverContent, GoopPopoverAnchor, GoopPopoverHeader, GoopPopoverTitle, and GoopPopoverDescription. All the goo lives on GoopPopoverContent.
Behavior
- Open (bloom). A droplet swells at the panel's own center and spreads out in every direction with a lobed edge until it fills the panel. It overshoots and settles with a jelly wobble (
wobble).bloomStart="merge"starts from a few drops that fuse instead of one. - Slosh and splash. Every edge bulges past rest, rebounds, and wobbles (
openSlosh), each side a little differently (sloshVariety). Drops splash off all around, fall, and fade, and none land on the trigger (openDrips). - Close. The panel pulls in, wobbles (
closeSlosh), and shrinks back to its center, faster than it opened. - Content stays sharp. Only the panel's background, ring, and shadow are goo. They're drawn on an
aria-hiddensurface behind your content, so text and inputs are never filtered. - Layout never changes. The goo draws in an overlay with
pointer-events: none.
Open styles
flowStyle="bloom" is the default. Two older opens are still there:
flowStyle="neck": goo bulges out of the trigger edge that faces the panel, runs across as a thick neck, swells into the panel, and pinches off. Close sucks it back into the trigger.:flow="false": a bulb swells at the trigger edge and the panel inflates out of it. The neck stretches and snaps (bridge,momentum).
bridge and momentum only apply to these two. The bloom ignores them.
<GoopPopoverContent bloom-start="merge">...</GoopPopoverContent>
<GoopPopoverContent flow-style="neck">...</GoopPopoverContent>
<GoopPopoverContent :flow="false">...</GoopPopoverContent>
Sides and alignment
side, align, and sideOffset are reka-ui's. The panel blooms wherever it lands, and the splash keeps off the trigger:
<GoopPopoverContent side="left" class="w-56">...</GoopPopoverContent>
<GoopPopoverContent align="end">...</GoopPopoverContent>
Surface color
By default the panel takes the trigger's color: the whole goo (panel, bridge, and drops) uses the trigger's background, and the content uses its text color. If the trigger has no background of its own, the theme's primary is used. Tokens inside the panel (muted text, borders, inputs) are retinted to match.
Set surface to change that. 'popover' uses shadcn's popover tokens, so the panel looks like a normal shadcn popover. Any CSS color works too:
<GoopPopoverContent>...</GoopPopoverContent> <!-- 'trigger' (default) -->
<GoopPopoverContent surface="popover">...</GoopPopoverContent>
<GoopPopoverContent surface="oklch(0.62 0.19 150)">...</GoopPopoverContent>
The colors are read again on every open, so a theme or trigger change is picked up.
Burst knobs
On open, the panel sheds drops. Four numeric props shape the burst. They only matter while openDrips is on:
openDripAmount(0 to 3): how many drops.openDripSize(0.3 to 2.5): how big.openDripFling(0 to 3): how far they fly.openDripVariety(0 to 1): the mix of big globs and specks. 0 makes them all alike.
sloshVariety (0 to 1, default 1) shapes the slosh instead: how differently each side bulges on open and close (off-centre, one or two lumps, staggered, re-rolled every open). 0 gives an even, centred bulge.
<GoopPopoverContent :open-drip-amount="2" :open-drip-size="1.6" :open-drip-fling="2" :open-drip-variety="1">
...
</GoopPopoverContent>
<GoopPopoverContent :open-drips="false">...</GoopPopoverContent>
openSlosh, openDrips, and closeSlosh take true, false, or a strength from 0 to 2.
Anchoring somewhere else
Put a GoopPopoverAnchor around any element and the popover positions against it, and the goo emerges from it, instead of the trigger:
<GoopPopover>
<GoopPopoverAnchor as-child>
<div class="rounded-lg border p-3">The goo comes out of this box</div>
</GoopPopoverAnchor>
<GoopPopoverTrigger as-child>
<GoopButton>Open</GoopButton>
</GoopPopoverTrigger>
<GoopPopoverContent>...</GoopPopoverContent>
</GoopPopover>
Plain shadcn
:goo="false" renders exactly shadcn's PopoverContent, with its own enter and exit animation:
<GoopPopoverContent :goo="false">...</GoopPopoverContent>
Driving it from code
GoopPopoverContent exposes its engine handle as handle (null before it mounts, or when goo is off). The panel only exists while open.
Props
The root, trigger, anchor, header, title, and description take shadcn-vue's props. GoopPopoverContent takes reka-ui's PopoverContent props (side, align, sideOffset, alignOffset, collisionPadding, and the rest), class, and these goo props. Defaults are align="center" and sideOffset="4", like shadcn.
| Prop | Type | Default | Notes |
|---|---|---|---|
goo | boolean | true | Master switch. false renders a plain shadcn PopoverContent. |
flow | boolean | true | Flow open, in flowStyle. false: the panel inflates from a bulb at the trigger edge (bridge and momentum apply). |
flowStyle | 'bloom' | 'neck' | 'bloom' | 'bloom' pools from the panel's own center, spreads out with a lobed edge, overshoots, and splashes all around, keeping drops off the trigger. 'neck' runs out of the trigger through a thick neck and pinches off. |
bloomStart | 'single' | 'merge' | 'single' | How the bloom starts: one droplet swells, or a few drops fuse. |
bridge | boolean | true | The bulb and lumpy neck between the trigger and the panel. Not used by bloom. |
wobble | boolean | true | Jelly overshoot and wobble (bloom: the open overshoot and settle). |
momentum | boolean | true | The panel launches away from the trigger, stretching the neck until it snaps. Not used by bloom. |
fade | boolean | true | The content fades in once the goo settles. |
openSlosh | boolean | number | true | On open, the far edge (bloom: every edge) bulges past rest, rebounds, and wobbles. A number from 0 to 2 sets the strength. |
openDrips | boolean | number | true | On open, the far edge (bloom: every edge) bursts: globs, drops, and specks fling off, fall, and fade. A number from 0 to 2 sets the strength. |
openDripAmount | number | 1 | Burst drop count, 0 to 3, times the preset's count. |
openDripSize | number | 1 | Burst drop size, 0.3 to 2.5. |
openDripFling | number | 1 | How far the drops are flung before they fall, 0 to 3. |
openDripVariety | number | 0.7 | Mix of big globs and specks, and how uneven the spray is, 0 to 1. 0 makes every drop alike. |
sloshVariety | number | 1 | How differently each side bulges on open and sloshes on close (off-centre, one or two lumps, staggered; re-rolled every open), 0 to 1. 0 is an even, centred bulge. |
closeSlosh | boolean | number | true | On close, the goo lags and the near edge sloshes against the trigger (bloom: the panel pulls in and wobbles before it shrinks to the center). A number from 0 to 2 sets the strength. |
surface | 'trigger' | 'popover' | string | 'trigger' | The panel's color. 'trigger' takes the trigger's background and text color, falling back to the theme's primary when the trigger has none. 'popover' uses shadcn's popover tokens. Any CSS color also works. |
speed | number | 1 | Animation speed multiplier. |
preset | 'subtle' | 'playful' | 'wild' | app config, then playful | The overall feel. |
renderer | 'svg' | 'canvas' | 'svg' | How the goo is drawn. |
Accessibility
Focus handling, Escape and outside-click dismissal, and ARIA attributes come from reka-ui. The goo surface is aria-hidden and can't be clicked, and your content is never filtered. Under reduced motion the popover opens and closes plainly: the panel keeps its goo shape and color, but the bloom, bridge, slosh, drops, and wobble are all turned off. See Accessibility & reduced motion.