Getting Started
Theming
Colors come from your shadcn tokens. The goo is tuned with a preset and --goop-* CSS variables, globally or per component.
Colors
goopUI uses the shadcn CSS variables you already have. A component's goo is painted in its variant's background token. GoopButton's default variant drips --primary, the destructive one drips --destructive, and so on. Change those tokens and the goo follows, in light and dark mode.
Under the hood, the engine paints with --goop-color (falling back to currentColor). The components set it for you. If you use @goopui/core directly, set it yourself:
.my-button {
--goop-color: var(--primary);
color: var(--primary-foreground);
background: transparent; /* the goo body paints the fill */
}
Presets
Start from a preset, then adjust single variables. playful is the default. subtle is calm enough for a dense dashboard: one slow drip, small reactions, no flying goo. wild is for hero sections: bigger and faster, with splashes that escape across the page, gloss, puddles, and long taffy.
.dashboard { --goop-preset: subtle; }
.hero { --goop-preset: wild; }
In JS it's preset: 'wild'. A preset only changes defaults, so anything you set explicitly still wins. This is everything the presets change:
| Variable | subtle | playful | wild |
|---|---|---|---|
| Wet gloss --goop-gloss | 0 | 0 | 0.5 |
| Drips per hover --goop-drip-count | 1 | 3 | 5 |
| Drip rhythm --goop-drip-interval | 2.2 | 0.9 | 0.5 |
| Puddles --goop-puddles | 0 | 0 | 1 |
| Puddle life --goop-puddle-life | 1.4 | 1.4 | 2.2 |
| Pointer reaction --goop-pointer-reaction | 0.45 | 1 | 1.6 |
| Inertia --goop-inertia | 0.5 | 1 | 1.6 |
| Flow to cursor --goop-flow-to-cursor | 0.4 | 1 | 1.3 |
| Scroll slosh --goop-scroll-slosh | 0 | 1 | 1.5 |
| Label ride --goop-label-ride | 0.3 | 0.6 | 0.8 |
| Press squish --goop-squish | 0.05 | 0.08 | 0.12 |
| Goop per click --goop-click-goop | 0.12 | 0.35 | 0.5 |
| Taffy pull --goop-taffy | 0 | 1 | 1.5 |
| Taffy snap length --goop-taffy-length | 90 | 90 | 150 |
| Reach distance --goop-reach | 0 | 0 | 110 |
| Splash speed --goop-splash-min-speed | 1 | 1 | 0.7 |
| Splash amount --goop-splash-amount | 0 | 1 | 2 |
| Click shockwave --goop-shockwave | 0.35 | 1 | 2 |
| Oiliness --goop-oiliness | 0.65 | 0.65 | 0.85 |
| Strand strength --goop-strands | 0 | 0.5 | 1 |
| Hit variety --goop-hit-variety | 0.7 | 0.7 | 0.9 |
| Splash reach --goop-splash | off | near | escape |
| Hit effect --goop-hit-effect | shockwave | shockwave | both |
Features
Every feature can be switched on or off by itself. A JS option takes true, false, or a strength; the CSS variable takes the strength, and 0 turns it off.
| Feature | Option | CSS variable | Default |
|---|---|---|---|
| Hover and tap drips | drips | --goop-drip-count | on |
| Click shockwave | shockwave | --goop-shockwave | on |
| Fast hits | hitEffect | --goop-hit-effect | shockwave |
| Splashes reach | splash | --goop-splash | near |
| Wet gloss | gloss | --goop-gloss | off |
| Taffy pull | taffy | --goop-taffy | on |
| Reach toward the pointer | reach | --goop-reach | on |
| Splash strands | strands | --goop-strands | on |
| Puddles | puddles | --goop-puddles | off |
| Label ride | labelRide | --goop-label-ride | on for [data-goop-label] content |
| Keyboard squish | keyboard | --goop-keyboard | on |
| Idle breathing | idle | --goop-idle | on |
| Oil finish | finish | --goop-finish | solid |
Hit effects
A fast pointer hit is a hard entry, or a fast swipe across or out of the element. --goop-hit-effect decides what it does:
shockwave(default): the same effect as a click, aimed along the motion and scaled by speed.splash: goo punches out of the far side.both, ornone.
Splash reach
--goop-splash decides where shockwave and splash particles go: near keeps them within about 70px of the element, escape flies them across the page in one shared overlay, and off has none. Click or swipe hard across the hero on the home page to see escape.
Oil finish
--goop-finish: oil gives the goo a thin-film iridescent sheen, like oil on a puddle. It's light by default; raise --goop-oil-intensity for more. It uses the canvas renderer, and you can switch it on and off at runtime with update({ finish }). Under reduced motion the sheen holds still.
.crude-oil {
--goop-color: oklch(0.17 0.01 60);
--goop-finish: oil;
--goop-oil-intensity: 0.6;
}
Every variable
Each knob is a CSS custom property. The engine reads them from the element (so they inherit) when it mounts, and again whenever update() runs. This table comes straight from the engine's own metadata, so names, defaults, ranges, and descriptions always match:
| Variable | Default | Range | What it does |
|---|---|---|---|
| Shape | |||
--goop-softnesssoftness | 4 | 0 to 40 px | Softness. How far apart shapes start to melt together; higher looks blobbier with rounder corners, lower looks crisper. Cost: none · Reduced motion: Unchanged (static). |
--goop-edge-sharpnessedgeSharpness | 20 | 1 to 100 x | Edge sharpness. How hard the goo outline is; lower gives a soft, slightly fuzzy edge. Cost: none · Reduced motion: Unchanged (static). |
--goop-edgeedge | 0.4 | 0.05 to 0.95 0-1 | Edge position. Where the outline sits on the melted shape; lower makes everything fatter and merge from further away, higher makes necks thinner. Cost: none · Reduced motion: Unchanged (static). |
--goop-glossgloss | 0 | 0 to 1 0-1 | Wet gloss. 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. Cost: low · Reduced motion: Unchanged (static). |
| Drips | |||
--goop-drip-countdripCount | 3 | 0 to 8 count | Drips per hover. Most drips hanging at once while hovered; 0 turns hover drips off. Cost: low · Reduced motion: Off. |
--goop-drip-lengthdripLength | 14 | 0 to 200 px | Drip length. How far a drip stretches below the edge before it lets go. Cost: none · Reduced motion: Unchanged (static). |
--goop-drip-sizedripSize | 7 | 1 to 60 px | Drip size. Radius of a drip bulb when it is full. Cost: none · Reduced motion: Unchanged (static). |
--goop-drip-intervaldripInterval | 0.9 | 0.1 to 10 s | Drip rhythm. Average time between new drips while the pointer rests on the element; moving and clicking make it faster. Cost: none · Reduced motion: Off. |
--goop-drip-variancedripVariance | 0.6 | 0 to 1 0-1 | Drip variety. How different drips look from each other (length, size, neck, spacing); 0 makes them identical. Cost: none · Reduced motion: Unchanged (static). |
--goop-resting-dripsrestingDrips | 0 | 0 to 8 count | Resting drips. Drips that always hang off the bottom edge, even when nothing happens. Cost: low · Reduced motion: Unchanged (static). |
--goop-idleidle | 1 | 0 to 1 on/off | Idle breathing. Resting drips swell very slowly so the element never looks frozen. Cost: low · Reduced motion: Off. |
--goop-max-dripsmaxDrips | 10 | 0 to 48 count | Drip cap. Hard limit on drips alive at once on this element; raise it for very wide elements. Cost: none · Reduced motion: Off. |
--goop-fall-distancefallDistance | 32 | 0 to 400 px | Fall distance. How far a released drop falls before it has drained away (when puddles are off). Cost: none · Reduced motion: Off. |
--goop-gravitygravity | 700 | 0 to 10000 px/s² | Gravity. How fast drops and splashes speed up as they fall. Cost: none · Reduced motion: Off. |
--goop-puddlespuddles | 0 | 0 to 1 on/off | Puddles. Opt-in: falling drops land on an invisible floor below the element, spread flat, then drain; off, they just fall and fade. Cost: low · Reduced motion: Off. |
--goop-puddle-lifepuddleLife | 1.4 | 0.2 to 10 s | Puddle life. How long a puddle lasts before it has fully drained. Cost: none · Reduced motion: Off. |
| Motion | |||
--goop-thicknessthickness | 1 | 0.2 to 5 x | Thickness. How syrupy the goo is: above 1 is slower and less bouncy like honey, below 1 is quicker and springier. Cost: none · Reduced motion: Unchanged (static). |
--goop-pointer-reactionpointerReaction | 1 | 0 to 4 x | Pointer reaction. Master strength for everything the pointer does to the goo (impact, lean, wobble, sway); 0 ignores pointer motion. Cost: none · Reduced motion: Off. |
--goop-inertiainertia | 1 | 0 to 4 x | Inertia. How much the goo sloshes behind sudden pointer moves, so drips swing and goo piles up on the leading side. Cost: none · Reduced motion: Off. |
--goop-flow-to-cursorflowToCursor | 1 | 0 to 2 x | Flow to cursor. How far hanging drips creep along the edge toward the cursor. Cost: none · Reduced motion: Off. |
--goop-scroll-sloshscrollSlosh | 1 | -3 to 3 x | Scroll slosh. Fast page scrolling sloshes the goo; negative flips the direction, 0 turns it off. Cost: low · Reduced motion: Off. |
--goop-tilt-strengthtiltStrength | 1 | 0 to 3 x | Tilt strength. How much tilting a phone pulls goo sideways (only with the tilt option on). Cost: low · Reduced motion: Off. |
--goop-label-ridelabelRide | 0.6 | 0 to 1 0-1 | Label ride. How much content marked [data-goop-label] moves and squishes along with the goo. Cost: low · Reduced motion: Off. |
| Touch | |||
--goop-squishsquish | 0.08 | 0 to 0.4 0-1 | Press squish. How much the body flattens while pressed. Cost: none · Reduced motion: Off. |
--goop-click-goopclickGoop | 0.35 | 0 to 2 x | Goop per click. Extra goo each click adds: the body swells, drips come faster and a splash drip falls. Cost: low · Reduced motion: Off. |
--goop-click-goop-fadeclickGoopFade | 1.2 | 0.1 to 10 s | Click goop fade. How long the extra goo from clicks takes to drain back to normal. Cost: none · Reduced motion: Off. |
--goop-keyboardkeyboard | 1 | 0 to 1 on/off | Keyboard squish. Space or Enter on the focused element squishes it from the top and adds goop. Cost: none · Reduced motion: Off. |
--goop-taffytaffy | 1 | 0 to 2 x | Taffy pull. Press and drag off the element to pull a stretchy goo string; 0 turns it off. Cost: low · Reduced motion: Off. |
--goop-taffy-lengthtaffyLength | 90 | 20 to 400 px | Taffy snap length. How far the string stretches past the edge before it snaps. Cost: none · Reduced motion: Off. |
--goop-reachreach | 0 | 0 to 300 px | Reach distance. 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. Cost: low · Reduced motion: Off. |
--goop-reach-sizereachSize | 2.6 | 0.5 to 5 x drip size | Reach lump size. How big the lump that reaches toward the pointer gets, as a multiple of the drip size. Cost: none · Reduced motion: Off. |
--goop-reach-globsreachGlobs | 3 | 1 to 4 count | Reach globs. Most lumps that swell toward an approaching pointer; extra ones join as it gets nearer or faster. Cost: low · Reduced motion: Off. |
--goop-taffy-strandstaffyStrands | 2 | 0 to 3 count | Taffy strands. Extra ropes that stretch from the edge into the pulled blob, each letting go at its own length. Cost: low · Reduced motion: Off. |
--goop-globbinessglobbiness | 0.8 | 0 to 1 0-1 | Globbiness. How fat and lumpy reach lumps and taffy pulls are; 1 is round blobby goo, 0 is a thin smooth string. Cost: none · Reduced motion: Off. |
| Splash | |||
--goop-splash-min-speedsplashMinSpeed | 1 | 0.1 to 5 x1200 px/s | Splash speed. How fast the pointer must enter (1 = 1200 px/s) to punch goo out of the far side. Cost: none · Reduced motion: Off. |
--goop-splash-amountsplashAmount | 1 | 0 to 3 x | Splash amount. How much goo a punch-through throws; 0 turns splashes off. Cost: medium · Reduced motion: Off. |
--goop-shockwaveshockwave | 1 | 0 to 3 x | Click shockwave. Strength of the ring of goo and the edge ripple on each click; 0 turns it off. Cost: medium · Reduced motion: Off. |
--goop-oilinessoiliness | 0.65 | 0 to 1 0-1 | Oiliness. Splash character: 0 is a watery spray of droplets, 1 is slow oily clumps that stretch. Cost: none · Reduced motion: Off. |
--goop-strandsstrands | 0.5 | 0 to 1 0-1 | Strand strength. How long splashes stay joined in strings before they snap; 0 gives loose clumps. Cost: low · Reduced motion: Off. |
--goop-hit-varietyhitVariety | 0.7 | 0 to 1 0-1 | Hit variety. 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. Cost: none · Reduced motion: Off. |
--goop-splashsplash | near | near · escape · off | Splash reach. Where splash goo may fly: near the element, across the whole page (escape), or nowhere. Cost: medium · Reduced motion: Off. |
--goop-hit-effecthitEffect | shockwave | shockwave · splash · both · none | Hit effect. 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. Cost: medium · Reduced motion: Off. |
| Features | |||
--goop-presetpreset | playful | subtle · playful · wild | Preset. Starting point for every feature: subtle for dense UIs, playful by default, wild for hero sections. Cost: none · Reduced motion: Motion features off whatever the preset. |
| Finish | |||
--goop-finishfinish | solid | solid · oil | Finish. Surface look: plain color, or an oily rainbow sheen at the edges. Cost: low · Reduced motion: Static sheen. |
--goop-oil-intensityoilIntensity | 0.3 | 0 to 1 0-1 | Oil sheen. How strong the oily rainbow sheen is. Cost: none · Reduced motion: Unchanged (static). |
--goop-oil-band-sizeoilBandSize | 1 | 0.1 to 10 x | Oil band size. Size of the rainbow bands; 2 makes them twice as large. Cost: none · Reduced motion: Unchanged (static). |
--goop-oil-hue-shiftoilHueShift | 0 | 0 to 1 cycles | Oil hue shift. Rotates which colors show in the sheen, like a slightly thicker or thinner oil film. Cost: none · Reduced motion: Unchanged (static). |
--goop-oil-follow-pointeroilFollowPointer | 0 | 0 to 2 0-2 | Oil follows pointer. Lets the rainbow bands drift and swirl with the mouse; 0 keeps them calm. Cost: low · Reduced motion: Off. |
Values are plain numbers: 6 and 6px both read as 6. Anything out of range is clamped, so a typo can't break the simulation.
Try it
Pick a preset, flip features, and move sliders. Each slider sets a variable on the demo area, and every slider explains what it does. The CSS below the demo is what you'd paste into your stylesheet.
/* The playful preset, as is. Move something. */Scope
Variables cascade like any custom property, so you can scope them however you like:
/* Everywhere */
:root {
--goop-thickness: 1.4;
}
/* Thicker goo, splashing across the page, in one section */
.hero {
--goop-preset: wild;
--goop-softness: 6;
--goop-drip-size: 9;
}
/* One component, calmer */
.settings .goop-button {
--goop-pointer-reaction: 0.4;
--goop-shockwave: 0;
}
Every engine option is also a component prop, so for the common cases you don't need CSS at all. See the Button page for the full list. The smaller components (Switch, Slider, Progress, Toaster, ...) take a short list of effect toggles, plus speed, preset, and renderer.
App-wide defaults
To set a preset or renderer for the whole app, use the goop options of @goopui/nuxt, or app.use(GoopPlugin, { preset: 'subtle' }) in plain Vue. See Installation. A prop on a component beats the app default.
Component colors
Components with a fill take their color from a shadcn token. Override it with a variable on the component or an ancestor:
| Component | Variable | Default |
|---|---|---|
| Button | --goop-color | the variant's background |
| Slider | --goop-slider-color | --primary |
| Progress | --goop-progress-color | --primary |
| Switch | --goop-switch-fill, --goop-switch-thumb | the checked track and the thumb |
| Checkbox | --goop-checkbox-fill | the checked fill |
| Radio group | --goop-radio-fill | the selected dot |
Popovers, menus, tooltips, and the Toaster paint the surface's own background, so change the shadcn token (--popover) instead.
Precedence
For each knob the engine uses the first one it finds:
- An option passed in JavaScript (
attachGoop(el, { softness: 6 })). - The
--goop-*variable on the element, which may be inherited. - The preset (
--goop-presetorpreset). - The built-in default.
Renamed variables
Some variables got clearer names. The old names still work as deprecated aliases (with a one-time warning in development), but new code should use the new ones:
| Old CSS var | New CSS var | Old option | New option |
|---|---|---|---|
--goop-blur | --goop-softness | blur | softness |
--goop-contrast | --goop-edge-sharpness | contrast | edgeSharpness |
--goop-threshold | --goop-edge | threshold | edge |
--goop-fall | --goop-fall-distance | fall | fallDistance |
--goop-viscosity | --goop-thickness | viscosity | thickness |
--goop-reactivity | --goop-pointer-reaction | reactivity | pointerReaction |
--goop-flow | --goop-flow-to-cursor | flow | flowToCursor |
--goop-tilt | --goop-tilt-strength | ||
--goop-click-mass | --goop-click-goop | clickMass | clickGoop |
--goop-mass-decay | --goop-click-goop-fade | massDecay | clickGoopFade |
--goop-splash-threshold | --goop-splash-min-speed | splashThreshold | splashMinSpeed |
--goop-stringiness | --goop-strands | stringiness | strands |
--goop-oil-scale | --goop-oil-band-size | oilScale | oilBandSize |
--goop-oil-shift | --goop-oil-hue-shift | oilShift | oilHueShift |
--goop-oil-react | --goop-oil-follow-pointer | oilReact | oilFollowPointer |