Components
Button
shadcn's Button with goo. Hover to drip, press to squish, tap for a single drip.
Customize
Installation
Nuxt
pnpm add @goopui/nuxt
export default defineNuxtConfig({
modules: ['@goopui/nuxt'],
})
The module auto-imports GoopButton, adds goop.css, and points Tailwind at the component classes. It needs Tailwind CSS v4 and the shadcn tokens (--primary, --secondary, --destructive, --border, --input, --background, --muted, --ring, --radius).
Vue
pnpm add @goopui/vue
@import "tailwindcss";
@import "@goopui/vue/goop.css";
import { createApp } from 'vue'
import { GoopPlugin } from '@goopui/vue'
import App from './App.vue'
createApp(App)
.use(GoopPlugin, { preset: 'playful' }) // optional: app-wide defaults
.mount('#app')
Usage
<script setup lang="ts">
// Nuxt auto-imports this. In plain Vue, import it:
import { GoopButton } from '@goopui/vue'
</script>
<template>
<GoopButton>Save</GoopButton>
<GoopButton variant="destructive">Delete</GoopButton>
</template>
Variants
The same variants as shadcn. The goo takes each variant's color: solid for default and secondary, a true tint for destructive, a 1px goo stroke for outline, and hover-only goo for ghost. Link has no goo, since a text link has no surface to drip from.
<GoopButton>Default</GoopButton>
<GoopButton variant="secondary">Secondary</GoopButton>
<GoopButton variant="destructive">Destructive</GoopButton>
<GoopButton variant="outline">Outline</GoopButton>
<GoopButton variant="ghost">Ghost</GoopButton>
<GoopButton variant="link">Link</GoopButton>
Behavior
- Hover spawns drips off the bottom edge. They swell and neck. If you keep hovering they detach, fall, shrink, and fade; if the pointer leaves first, they get pulled back up. Resting the pointer keeps a slow drip going (
--goop-drip-interval). - Reach. Before the pointer even arrives, one to three lumps swell out of the edge toward it (
--goop-reach,--goop-reach-globs,--goop-reach-size). Come in slowly from below to see them. - Speed matters. A fast entry spawns more drips in a tighter cluster, and a fast hit (past
--goop-splash-min-speed) fires a click-like shockwave aimed along the motion (--goop-hit-effect). - Momentum. The body leans and wobbles with the pointer, hanging drips swing with its acceleration (
--goop-inertia) and creep along the edge toward the cursor (--goop-flow-to-cursor), and fast page scrolls make them slosh (--goop-scroll-slosh). - Taffy. Press and drag off the button to pull a stretchy string of goo, with a couple of extra strands, until it snaps (
--goop-taffy,--goop-taffy-length,--goop-taffy-strands). On touch it starts after a long press, so scrolling still works. - Label ride. The label moves and squishes with the body; transforms only, so the text stays sharp (
--goop-label-ride). - Click squishes the body, then on release adds goo (it swells and drips faster) and fires a shockwave from the click point. Space and Enter do the same from the top.
- Touch gives a squish, a drip, and a shockwave per tap. Hover drips only run on devices with a real hover.
- Layout never changes. Drips and particles draw in an absolutely positioned overlay with
pointer-events: none.
Sizes and shapes
The sizes are shadcn's. The goo follows the element's own box and border-radius, so a pill is just rounded-full:
<GoopButton size="xs">Extra small</GoopButton>
<GoopButton size="sm">Small</GoopButton>
<GoopButton>Default</GoopButton>
<GoopButton size="lg">Large</GoopButton>
<GoopButton size="icon" aria-label="Next"><ArrowRight /></GoopButton>
<GoopButton class="rounded-full">Pill</GoopButton>
As a link
asChild renders your element instead of a <button>, with the same classes and goo. Use it with NuxtLink (or RouterLink):
<GoopButton as-child>
<NuxtLink to="/docs/getting-started/installation">Installation guide</NuxtLink>
</GoopButton>
Presets
subtle, playful (default), and wild set every feature at once. Use subtle in dense UIs, wild for a hero CTA:
<GoopButton preset="subtle">Save</GoopButton>
<GoopButton preset="wild">Get started</GoopButton>
Or from CSS: --goop-preset: wild. The Theming page lists everything each preset changes. To try them on these buttons, use the tuner there.
Feature toggles
Every engine feature is a prop. A boolean switches it off (false) or on at the preset's strength (true), and a number tunes it. Leave it out and the CSS vars and the preset decide.
<GoopButton :shockwave="false" taffy>Taffy, no shockwave</GoopButton>
<GoopButton gloss reach>Gloss and reach</GoopButton>
<GoopButton preset="wild" variant="secondary">Wild</GoopButton>
<GoopButton preset="subtle" variant="outline">Subtle</GoopButton>
<GoopButton :drip-size="12" :inertia="0.4">Tuned with numbers</GoopButton>
For anything not covered by a prop, the goop prop takes a whole options object, and explicit props win over it:
<GoopButton :goop="{ splash: 'escape', restingDrips: [0.3, 0.7] }">Escape artist</GoopButton>
Resting drips
restingDrips hangs still drips off the edge, and they breathe very slowly (--goop-idle). Give it a count, or place them as fractions of the width, or as { x, size, length } objects for an organic, uneven edge. They're also what's left in reduced-motion mode:
<GoopButton :resting-drips="1">One drip</GoopButton>
<GoopButton variant="destructive" :resting-drips="2">Two drips</GoopButton>
<GoopButton class="rounded-full" :resting-drips="[0.2, 0.32, 0.8]" :drip-length="20">
Placed: 0.2, 0.32, 0.8
</GoopButton>
Oil finish
finish="oil" (or --goop-finish: oil) gives the goo a thin-film sheen. It works on any color, and is strongest on dark ink:
<GoopButton
size="lg"
finish="oil"
:oil-intensity="0.6"
class="bg-neutral-950 text-white [--goop-color:var(--color-neutral-950)]"
>
Crude oil
</GoopButton>
<GoopButton size="lg" variant="destructive" finish="oil">Oil on red</GoopButton>
Disabled and drip-free
A disabled button ignores the pointer. Drips already running finish, and nothing new starts. :drips="false" keeps the press squish, and :goo="false" renders a plain shadcn Button:
<GoopButton disabled>Disabled</GoopButton>
<GoopButton :drips="false">No drips, squish only</GoopButton>
<GoopButton :goo="false" variant="outline">Plain shadcn (goo off)</GoopButton>
Driving it from code
GoopButton exposes the engine handle and its methods, so you can drip or splash it from a script, like the Drip button under the demos above does:
<script setup lang="ts">
const btn = useTemplateRef('btn')
</script>
<template>
<GoopButton ref="btn" :time-scale="0.3">Slow</GoopButton>
<button @click="btn?.drip()">Drip it</button>
</template>
It exposes handle, el, refresh(), press(), release(), hover(on, x?), drip(x?), addGoop(), shockwave(), splashThrough(), feedPointer(), and inspect(). After changing --goop-* vars on an ancestor, call refresh() so the button re-reads them.
Props
GoopButton keeps the shadcn-vue Button API and adds the goo props.
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'default' | Same as shadcn. The goo takes the variant's color: tinted for destructive, a goo stroke for outline, hover-only for ghost. Link has no goo. |
size | 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg' | 'default' | Same as shadcn. |
asChild | boolean | false | Render your own child (a NuxtLink, an <a>) with the button's classes and goo, via reka-ui Primitive. |
as | string | Component | 'button' | The element to render. |
class | string | array | object | Merged onto the root, like shadcn. Utilities such as bg-*, rounded-*, and [--goop-color:...] win over the variant. | |
disabled | boolean | false | Disables the button. Drips already running finish; nothing new starts. |
goo | boolean | true (false for link) | Master switch. false renders a plain shadcn Button. |
goop | GoopOptions | Escape hatch: any engine option as an object. The explicit props win over it. |
Engine props
Every engine option is a prop, in camelCase (dripSize becomes :drip-size). This table is generated from the engine's own metadata, so it always matches the version of the docs you're reading. Each prop is also a --goop-* CSS variable. Precedence, high to low: prop, then the goop object, then CSS var, then preset, then default.
| Prop | Type | Default | What it does |
|---|---|---|---|
| Shape | |||
softness--goop-softness | number 0 to 40 px | 4 | How far apart shapes start to melt together; higher looks blobbier with rounder corners, lower looks crisper. |
edgeSharpness--goop-edge-sharpness | number 1 to 100 x | 20 | How hard the goo outline is; lower gives a soft, slightly fuzzy edge. |
edge--goop-edge | number 0.05 to 0.95 0-1 | 0.4 | Where the outline sits on the melted shape; lower makes everything fatter and merge from further away, higher makes necks thinner. |
gloss--goop-gloss | boolean | number 0 to 1 0-1 | 0 | Opt-in wet look: a soft top-left highlight and rim shading so the goo looks 3D. Off (flat color) by default; `gloss: true` uses 0.6. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
| Drips | |||
dripCount--goop-drip-count | number 0 to 8 count | 3 | Most drips hanging at once while hovered; 0 turns hover drips off. |
dripLength--goop-drip-length | number 0 to 200 px | 14 | How far a drip stretches below the edge before it lets go. |
dripSize--goop-drip-size | number 1 to 60 px | 7 | Radius of a drip bulb when it is full. |
dripInterval--goop-drip-interval | number 0.1 to 10 s | 0.9 | Average time between new drips while the pointer rests on the element; moving and clicking make it faster. |
dripVariance--goop-drip-variance | number 0 to 1 0-1 | 0.6 | How different drips look from each other (length, size, neck, spacing); 0 makes them identical. |
restingDrips--goop-resting-drips | number | number[] | object[] 0 to 8 count | 0 | Drips that always hang off the bottom edge, even when nothing happens. |
idle--goop-idle | boolean | number 0 to 1 on/off | 1 | Resting drips swell very slowly so the element never looks frozen. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
maxDrips--goop-max-drips | number 0 to 48 count | 10 | Hard limit on drips alive at once on this element; raise it for very wide elements. |
fallDistance--goop-fall-distance | number 0 to 400 px | 32 | How far a released drop falls before it has drained away (when puddles are off). |
gravity--goop-gravity | number 0 to 10000 px/s² | 700 | How fast drops and splashes speed up as they fall. |
puddles--goop-puddles | boolean | number 0 to 1 on/off | 0 | Opt-in: falling drops land on an invisible floor below the element, spread flat, then drain; off, they just fall and fade. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
puddleLife--goop-puddle-life | number 0.2 to 10 s | 1.4 | How long a puddle lasts before it has fully drained. |
| Motion | |||
thickness--goop-thickness | number 0.2 to 5 x | 1 | How syrupy the goo is: above 1 is slower and less bouncy like honey, below 1 is quicker and springier. |
pointerReaction--goop-pointer-reaction | boolean | number 0 to 4 x | 1 | Master strength for everything the pointer does to the goo (impact, lean, wobble, sway); 0 ignores pointer motion. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
inertia--goop-inertia | boolean | number 0 to 4 x | 1 | How much the goo sloshes behind sudden pointer moves, so drips swing and goo piles up on the leading side. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
flowToCursor--goop-flow-to-cursor | boolean | number 0 to 2 x | 1 | How far hanging drips creep along the edge toward the cursor. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
scrollSlosh--goop-scroll-slosh | boolean | number -3 to 3 x | 1 | Fast page scrolling sloshes the goo; negative flips the direction, 0 turns it off. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
tiltStrength--goop-tilt-strength | number 0 to 3 x | 1 | How much tilting a phone pulls goo sideways (only with the tilt option on). |
labelRide--goop-label-ride | boolean | number 0 to 1 0-1 | 0.6 | How much content marked [data-goop-label] moves and squishes along with the goo. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
| Touch | |||
squish--goop-squish | boolean | number 0 to 0.4 0-1 | 0.08 | How much the body flattens while pressed. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
clickGoop--goop-click-goop | boolean | number 0 to 2 x | 0.35 | Extra goo each click adds: the body swells, drips come faster and a splash drip falls. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
clickGoopFade--goop-click-goop-fade | number 0.1 to 10 s | 1.2 | How long the extra goo from clicks takes to drain back to normal. |
keyboard--goop-keyboard | boolean | number 0 to 1 on/off | 1 | Space or Enter on the focused element squishes it from the top and adds goop. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
taffy--goop-taffy | boolean | number 0 to 2 x | 1 | Press and drag off the element to pull a stretchy goo string; 0 turns it off. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
taffyLength--goop-taffy-length | number 20 to 400 px | 90 | How far the string stretches past the edge before it snaps. |
reach--goop-reach | boolean | number 0 to 300 px | 0 | Opt-in: a lump of goo swells out of the edge toward a pointer closer than this, before it touches. Off (0) by default; `reach: true` uses 80. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
reachSize--goop-reach-size | number 0.5 to 5 x drip size | 2.6 | How big the lump that reaches toward the pointer gets, as a multiple of the drip size. |
reachGlobs--goop-reach-globs | number 1 to 4 count | 3 | Most lumps that swell toward an approaching pointer; extra ones join as it gets nearer or faster. |
taffyStrands--goop-taffy-strands | number 0 to 3 count | 2 | Extra ropes that stretch from the edge into the pulled blob, each letting go at its own length. |
globbiness--goop-globbiness | number 0 to 1 0-1 | 0.8 | How fat and lumpy reach lumps and taffy pulls are; 1 is round blobby goo, 0 is a thin smooth string. |
| Splash | |||
splashMinSpeed--goop-splash-min-speed | number 0.1 to 5 x1200 px/s | 1 | How fast the pointer must enter (1 = 1200 px/s) to punch goo out of the far side. |
splashAmount--goop-splash-amount | boolean | number 0 to 3 x | 1 | How much goo a punch-through throws; 0 turns splashes off. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
shockwave--goop-shockwave | boolean | number 0 to 3 x | 1 | Strength of the ring of goo and the edge ripple on each click; 0 turns it off. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
oiliness--goop-oiliness | number 0 to 1 0-1 | 0.65 | Splash character: 0 is a watery spray of droplets, 1 is slow oily clumps that stretch. |
strands--goop-strands | boolean | number 0 to 1 0-1 | 0.5 | How long splashes stay joined in strings before they snap; 0 gives loose clumps. Boolean: false turns it off, true uses the preset's strength, a number tunes it. |
hitVariety--goop-hit-variety | number 0 to 1 0-1 | 0.7 | How different each punch-through looks: big lead globs, medium drops and fine specks, a varied cone, tear-off spot and neck; 0 makes every hit the same. |
splash--goop-splash | 'near' | 'escape' | 'off' | near | Where splash goo may fly: near the element, across the whole page (escape), or nowhere. |
hitEffect--goop-hit-effect | 'shockwave' | 'splash' | 'both' | 'none' | shockwave | What a fast pointer hit does (a hard entry or a fast swipe across or out): a click-like shockwave, the punch-through glob, both, or nothing. |
| Features | |||
preset--goop-preset | 'subtle' | 'playful' | 'wild' | playful | Starting point for every feature: subtle for dense UIs, playful by default, wild for hero sections. |
| Finish | |||
finish--goop-finish | 'solid' | 'oil' | solid | Surface look: plain color, or an oily rainbow sheen at the edges. |
oilIntensity--goop-oil-intensity | number 0 to 1 0-1 | 0.3 | How strong the oily rainbow sheen is. |
oilBandSize--goop-oil-band-size | number 0.1 to 10 x | 1 | Size of the rainbow bands; 2 makes them twice as large. |
oilHueShift--goop-oil-hue-shift | number 0 to 1 cycles | 0 | Rotates which colors show in the sheen, like a slightly thicker or thinner oil film. |
oilFollowPointer--goop-oil-follow-pointer | number 0 to 2 0-2 | 0 | Lets the rainbow bands drift and swirl with the mouse; 0 keeps them calm. |
| Also | |||
renderer | 'svg' | 'canvas' | 'svg' | How the goo is drawn. finish="oil" upgrades to canvas. |
drips | boolean | true | false keeps the goo body and squish but never drips. |
interactive | boolean | true | false ignores the pointer (static or script-driven goo). |
tilt | boolean | true | Lean toward the pointer. |
reducedMotion | boolean | 'auto' | 'auto' | Force reduced motion on, or ignore the OS setting with false. |
seed | number | per instance | Seed for the random drips, so a button looks the same on every render. |
timeScale | number | 1 | Per-instance time scale: 0.3 runs this button in slow motion. |
escapeRenderer | 'svg' | 'canvas' | 'canvas' | Renderer for splashes that leave the button. |
Accessibility
It's a real <button> (or your asChild element). The goo layer is aria-hidden and can't be clicked, the label is never filtered, and your focus ring isn't covered. Space and Enter get the same squish and shockwave as a click. Under reduced motion the shape and resting drips stay; drips, squish, momentum, reach, taffy, splashes, and shockwaves turn off, and an oil sheen holds still. See Accessibility & reduced motion.