Theming

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:

Variablesubtleplayfulwild
Wet gloss
--goop-gloss
000.5
Drips per hover
--goop-drip-count
135
Drip rhythm
--goop-drip-interval
2.20.90.5
Puddles
--goop-puddles
001
Puddle life
--goop-puddle-life
1.41.42.2
Pointer reaction
--goop-pointer-reaction
0.4511.6
Inertia
--goop-inertia
0.511.6
Flow to cursor
--goop-flow-to-cursor
0.411.3
Scroll slosh
--goop-scroll-slosh
011.5
Label ride
--goop-label-ride
0.30.60.8
Press squish
--goop-squish
0.050.080.12
Goop per click
--goop-click-goop
0.120.350.5
Taffy pull
--goop-taffy
011.5
Taffy snap length
--goop-taffy-length
9090150
Reach distance
--goop-reach
00110
Splash speed
--goop-splash-min-speed
110.7
Splash amount
--goop-splash-amount
012
Click shockwave
--goop-shockwave
0.3512
Oiliness
--goop-oiliness
0.650.650.85
Strand strength
--goop-strands
00.51
Hit variety
--goop-hit-variety
0.70.70.9
Splash reach
--goop-splash
offnearescape
Hit effect
--goop-hit-effect
shockwaveshockwaveboth

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.

FeatureOptionCSS variableDefault
Hover and tap dripsdrips--goop-drip-counton
Click shockwaveshockwave--goop-shockwaveon
Fast hitshitEffect--goop-hit-effectshockwave
Splashes reachsplash--goop-splashnear
Wet glossgloss--goop-glossoff
Taffy pulltaffy--goop-taffyon
Reach toward the pointerreach--goop-reachon
Splash strandsstrands--goop-strandson
Puddlespuddles--goop-puddlesoff
Label ridelabelRide--goop-label-rideon for [data-goop-label] content
Keyboard squishkeyboard--goop-keyboardon
Idle breathingidle--goop-idleon
Oil finishfinish--goop-finishsolid

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, or none.

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:

VariableDefaultRangeWhat it does
Shape
--goop-softness
softness
40 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-sharpness
edgeSharpness
201 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-edge
edge
0.40.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-gloss
gloss
00 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-count
dripCount
30 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-length
dripLength
140 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-size
dripSize
71 to 60
px
Drip size. Radius of a drip bulb when it is full.
Cost: none · Reduced motion: Unchanged (static).
--goop-drip-interval
dripInterval
0.90.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-variance
dripVariance
0.60 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-drips
restingDrips
00 to 8
count
Resting drips. Drips that always hang off the bottom edge, even when nothing happens.
Cost: low · Reduced motion: Unchanged (static).
--goop-idle
idle
10 to 1
on/off
Idle breathing. Resting drips swell very slowly so the element never looks frozen.
Cost: low · Reduced motion: Off.
--goop-max-drips
maxDrips
100 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-distance
fallDistance
320 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-gravity
gravity
7000 to 10000
px/s²
Gravity. How fast drops and splashes speed up as they fall.
Cost: none · Reduced motion: Off.
--goop-puddles
puddles
00 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-life
puddleLife
1.40.2 to 10
s
Puddle life. How long a puddle lasts before it has fully drained.
Cost: none · Reduced motion: Off.
Motion
--goop-thickness
thickness
10.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-reaction
pointerReaction
10 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-inertia
inertia
10 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-cursor
flowToCursor
10 to 2
x
Flow to cursor. How far hanging drips creep along the edge toward the cursor.
Cost: none · Reduced motion: Off.
--goop-scroll-slosh
scrollSlosh
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-strength
tiltStrength
10 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-ride
labelRide
0.60 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-squish
squish
0.080 to 0.4
0-1
Press squish. How much the body flattens while pressed.
Cost: none · Reduced motion: Off.
--goop-click-goop
clickGoop
0.350 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-fade
clickGoopFade
1.20.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-keyboard
keyboard
10 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-taffy
taffy
10 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-length
taffyLength
9020 to 400
px
Taffy snap length. How far the string stretches past the edge before it snaps.
Cost: none · Reduced motion: Off.
--goop-reach
reach
00 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-size
reachSize
2.60.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-globs
reachGlobs
31 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-strands
taffyStrands
20 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-globbiness
globbiness
0.80 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-speed
splashMinSpeed
10.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-amount
splashAmount
10 to 3
x
Splash amount. How much goo a punch-through throws; 0 turns splashes off.
Cost: medium · Reduced motion: Off.
--goop-shockwave
shockwave
10 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-oiliness
oiliness
0.650 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-strands
strands
0.50 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-variety
hitVariety
0.70 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-splash
splash
nearnear · 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-effect
hitEffect
shockwaveshockwave · 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-preset
preset
playfulsubtle · 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-finish
finish
solidsolid · oil Finish. Surface look: plain color, or an oily rainbow sheen at the edges.
Cost: low · Reduced motion: Static sheen.
--goop-oil-intensity
oilIntensity
0.30 to 1
0-1
Oil sheen. How strong the oily rainbow sheen is.
Cost: none · Reduced motion: Unchanged (static).
--goop-oil-band-size
oilBandSize
10.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-shift
oilHueShift
00 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-pointer
oilFollowPointer
00 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.

Preset
Splash reach
Hit effect
Finish
Hover, approach slowly, swipe, click, press and drag off.
/* 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:

ComponentVariableDefault
Button--goop-colorthe variant's background
Slider--goop-slider-color--primary
Progress--goop-progress-color--primary
Switch--goop-switch-fill, --goop-switch-thumbthe checked track and the thumb
Checkbox--goop-checkbox-fillthe checked fill
Radio group--goop-radio-fillthe 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:

  1. An option passed in JavaScript (attachGoop(el, { softness: 6 })).
  2. The --goop-* variable on the element, which may be inherited.
  3. The preset (--goop-preset or preset).
  4. 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 varNew CSS varOld optionNew option
--goop-blur--goop-softnessblursoftness
--goop-contrast--goop-edge-sharpnesscontrastedgeSharpness
--goop-threshold--goop-edgethresholdedge
--goop-fall--goop-fall-distancefallfallDistance
--goop-viscosity--goop-thicknessviscositythickness
--goop-reactivity--goop-pointer-reactionreactivitypointerReaction
--goop-flow--goop-flow-to-cursorflowflowToCursor
--goop-tilt--goop-tilt-strength
--goop-click-mass--goop-click-goopclickMassclickGoop
--goop-mass-decay--goop-click-goop-fademassDecayclickGoopFade
--goop-splash-threshold--goop-splash-min-speedsplashThresholdsplashMinSpeed
--goop-stringiness--goop-strandsstringinessstrands
--goop-oil-scale--goop-oil-band-sizeoilScaleoilBandSize
--goop-oil-shift--goop-oil-hue-shiftoilShiftoilHueShift
--goop-oil-react--goop-oil-follow-pointeroilReactoilFollowPointer