Core
@goopui/core API
The framework-agnostic engine under every goopUI component. Attach goo to any element, then tune it, drive it, and tear it down.
@goopui/core is plain TypeScript and DOM. It has no framework and no dependencies, and it never touches document or window at import time, so it's safe to import during SSR. The Vue, React, and Svelte packages are thin wrappers around it. You only need it directly to put goo on something that isn't a goopUI component, or to build an adapter.
attachGoop
function attachGoop(host: HTMLElement, options?: GoopOptions): GoopHandle
This turns host into a goo body. It adds an aria-hidden goo layer inside the host at z-index: -1 and sets isolation: isolate on the host (plus position: relative if it was static). Your content paints above the goo, unfiltered. Give the host a transparent background and set --goop-color:
import { attachGoop } from '@goopui/core'
const button = document.querySelector<HTMLButtonElement>('.save')!
button.style.setProperty('--goop-color', 'var(--primary)')
const goop = attachGoop(button, { dripCount: 2, splash: 'escape' })
// later
goop.update() // re-read --goop-* variables
goop.destroy()
Out of the box an attached element:
- drips on hover, more and faster the harder the pointer comes in;
- leans, wobbles, and carries momentum as the pointer moves (
inertia), and its hanging drips creep toward the cursor (flow); - swells a lump or two toward an approaching pointer before it even touches (
reach); - fires a click-like shockwave, aimed along the motion, when the pointer hits it fast: a hard entry or a fast swipe across or out (
hitEffect,splashMinSpeed); - squishes on press, and on release adds goo (
clickGoop) and fires a shockwave from the click point (shockwave); - pulls a stretchy taffy string when you press and drag off it (
taffy); - does the same press from the keyboard: Space or Enter squishes from the top (
keyboard); - sloshes when the page scrolls (
scrollSlosh), and its resting drips breathe slowly (idle).
preset: 'subtle' | 'playful' | 'wild' sets the starting point for all of it. See Theming.
Old option and variable names (blur, viscosity, --goop-threshold, ...) still work as deprecated aliases. See renamed variables.
In Vue, attach on mount and destroy on unmount. This site's demos use a wrapper like this one:
<script setup lang="ts">
import { attachGoop, type GoopHandle, type GoopOptions } from '@goopui/core'
import { onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps<{ options?: GoopOptions }>()
const el = ref<HTMLElement | null>(null)
let handle: GoopHandle | null = null
onMounted(() => {
if (el.value) handle = attachGoop(el.value, { ...props.options })
})
watch(() => props.options, (o) => handle?.update({ ...o }), { deep: true })
onUnmounted(() => handle?.destroy())
</script>
<template>
<button ref="el" class="goop-button"><slot /></button>
</template>
GoopOptions
GoopOptions takes every tuning knob as an optional number (softness, dripCount, inertia, shockwave, and so on), plus:
| Option | Type | Default | Description |
|---|---|---|---|
preset | 'subtle' | 'playful' | 'wild' | 'playful' | Defaults for every feature. Also --goop-preset. Explicit options and variables still win. |
restingDrips | number | (number | { x, size?, length?, pinch?, phase? })[] | 0 | Still drips on the bottom edge. A count spaces them evenly; an array places them at 0 to 1 fractions of the width. Objects also set each drip's size and length as multipliers, for an uneven, organic edge. --goop-drip-variance adds natural variety. |
hitEffect | 'shockwave' | 'splash' | 'both' | 'none' | 'shockwave' | What a fast pointer hit does. Also --goop-hit-effect. |
gloss | boolean | number | false | Wet look: a soft highlight and rim shading. true uses 0.6. |
taffy | boolean | number | true | Press and drag off to pull a stretchy string. Tune with taffyLength, taffyStrands, globbiness. |
reach | boolean | number | true | Lumps swell toward a pointer closer than this many px. Tune with reachSize, reachGlobs. |
puddles | boolean | false | Falling drops land, spread flat, and drain (puddleLife). |
labelRide | boolean | number | true | Content marked [data-goop-label] moves and squishes with the goo (transform only, so text stays crisp). |
idle | boolean | true | Resting drips breathe with a very slow swell. |
seed | number | from the element | Seed for the resting-drip layout, so SSR and re-renders agree. |
splash | 'near' | 'escape' | 'off' | 'near' | Where splash and shockwave particles go. escape flies them across the page in a shared overlay. Also --goop-splash. |
finish | 'solid' | 'oil' | 'solid' | Surface finish. oil is a thin-film iridescent sheen and picks the canvas renderer. Also --goop-finish. |
renderer | 'svg' | 'canvas' | factory | 'svg' | How the goo is drawn. See Renderers. Can be switched with update(). |
escapeRenderer | 'svg' | 'canvas' | factory | 'canvas' | Renderer for the shared page overlay that escape splashes fly in. |
timeScale | number | 1 | Per-element speed. 0.3 is slow motion for just this element, multiplied with the global setGoopTimeScale. |
maxDrips | number | 10 | Cap on live drips. Also --goop-max-drips. |
drips | boolean | true | Spawn drips. false keeps the body, squish, splash, and shockwave. |
interactive | boolean | true | Listen to pointer events on the host. Set it to false to drive the goo yourself with the handle. |
keyboard | boolean | true | Space and Enter on the focused host squish it from the top and add goo. |
disabled | boolean | false | Ignore interaction. Drips already running finish, and nothing new starts. |
reducedMotion | boolean | 'auto' | 'auto' | 'auto' follows prefers-reduced-motion and <html data-reduced-motion="true">. true or false forces it. |
tilt | boolean | false | Device tilt shifts gravity sideways on phones. Call requestTiltPermission() from a user gesture first on iOS. |
cssVars | boolean | false | Write the body's pose to the host each frame as --goop-x, --goop-y (px), --goop-skew (deg), --goop-sx, and --goop-sy, so your content can ride along with the goo. |
random | () => number | Math.random | RNG for drip placement and jitter. Pass a seeded one for tests. |
Tuning precedence: a JS option, then the --goop-* variable on the host, then the default. Values are clamped to GOOP_LIMITS.
GoopHandle
| Member | Description |
|---|---|
update(opts?) | Merge new options and re-read the --goop-* variables from the host. Changing renderer or finish swaps the renderer in place: live drips, particles, puddles, and taffy keep going. |
destroy() | Remove listeners, observers, the rAF ticker, the DOM, and the filter or canvas. Restores the host's inline position and isolation. Safe to call twice. |
press() / release() | Squish the body, then release it with a spring rebound, like a pointer press. |
hover(on, x?) | true starts hover drips (optionally near x, in host px). false pulls back drips that haven't detached. |
drip(x?) | Spawn one drip that swells and detaches on its own, like a tap. |
addGoop(amount?, x?) | Add goo like a click does: swell the body and throw a drip at x. amount defaults to --goop-click-goop. |
shockwave(x?, y?, strength?, aim?) | Fire a click shockwave at (x, y) in host px (default: the center): a particle ring and an edge ripple. aim: { vx, vy } (px/s) points it, for hosts that never see the pointer. |
feedPointer({ clientX, clientY }) | Drive the pointer from outside, for interactive: false hosts. While hover(true) is on it steers lean, inertia, flow, and drip placement. |
splashThrough(x, y, vx, vy) | Punch goo through as if the pointer entered at (x, y) moving at (vx, vy) px/s. Particles go where splash says. |
inspect() | What's driving the goo right now: pointer velocity, added mass, live drips and particles, slosh, scroll, splash mode. |
layer | The aria-hidden goo layer element (read-only). |
reducedMotion | Whether the instance is currently in still mode (read-only). |
tuning | The resolved tuning in use (read-only). |
Driving a goo body by hand, for an element that can't take pointer events itself (a decorative strip, a layer under other content):
const edge = attachGoop(strip, {
interactive: false,
restingDrips: [{ x: 0.25, size: 1.2, length: 1.4 }, { x: 0.7, size: 1.5, length: 2 }],
splash: 'escape',
})
area.addEventListener('pointerenter', () => edge.hover(true))
area.addEventListener('pointerleave', () => edge.hover(false))
area.addEventListener('pointermove', (e) => edge.feedPointer(e))
area.addEventListener('click', (e) => {
const r = strip.getBoundingClientRect()
// Aim the ring along the pointer's last motion.
edge.shockwave(e.clientX - r.left, r.height / 2, 1.5, { vx: 1200, vy: 300 })
})
The hero on the home page doesn't need any of this: the whole panel is one interactive goo body on the canvas renderer.
Tuning
These are the per-element knobs. Each one maps to a --goop-* variable:
| 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. |
| Export | Description |
|---|---|
GOOP_VAR_META | One entry per variable, keyed by option: name, option, label, description, unit, default, range and step, group, cost, reduced-motion behavior, and old aliases. The docs table and the tuner on this site are built from it. |
GOOP_PRESETS, GOOP_PRESET_VAR, GOOP_DEFAULT_PRESET, readPreset | The presets and how they resolve. |
GOOP_VAR_RENAMES, GOOP_OPTION_RENAMES, renameOptions | Old variable and option names mapped to the new ones. |
GOOP_DEFAULTS | Default value for each knob (frozen). |
GOOP_CSS_VARS | The CSS variable name for each knob, such as softness → --goop-softness. |
GOOP_LIMITS | The [min, max] clamp for each knob. |
GOOP_SPLASH_VAR, GOOP_DEFAULT_SPLASH, GOOP_HIT_VAR, GOOP_DEFAULT_HIT | --goop-splash and --goop-hit-effect, with their defaults ('near' and 'shockwave'). |
GOOP_FINISH_VARS, GOOP_FINISH_DEFAULTS, readFinish(el, overrides?) | The oil finish variables, their defaults, and the resolver. |
GOOP_PHYSICS | Engine-level feel constants like spring responses, swell and drain times, and drop drag. They aren't exposed as CSS variables. |
readTuning(el, overrides?) | Resolve the tuning for an element: defaults, then CSS variables, then overrides, all clamped. |
GoopTuning, GoopVarMeta, GoopPreset, GoopHitEffect, GoopSplashMode, GoopFinishName, SpringFeel | Types. A SpringFeel is { response, dampingRatio }, the way Apple-style springs are tuned. |
Renderers
Two renderers draw the same simulation, so a knob means the same thing in both:
attachGoop(el) // 'svg' (default): SVG goo filter on DOM spans
attachGoop(el, { renderer: 'canvas' }) // WebGL2 metaballs, Canvas 2D fallback
attachGoop(el, { renderer: makeCanvasRenderer({ backend: '2d' }) })
attachGoop(el, { finish: 'oil' }) // picks 'canvas' for you
- SVG draws plain
<span>s (valid inside a<button>) in a filtered layer and only changestransformandopacityeach frame. - Canvas gives each element its own small
<canvas>in the same layer, so scrolling, clipping, and stacking don't change. One shared WebGL2 context per document draws every element in a single batch. Without WebGL2, or after a context loss, it falls back to Canvas 2D with the same filter and switches back when the context returns. It reads--goop-colorfrom computed style and repaints on theme and class changes; callupdate()after any other color change made while the goo is idle.
Escape splashes fly in one shared, fixed, full-viewport overlay (acquireGoopOverlay). You can use it too, for goo that has to leave its component.
| Export | Description |
|---|---|
createSvgFilterRenderer, createCanvasRenderer, makeCanvasRenderer(opts) | The built-in renderer factories. |
resolveRenderer(option) | Turn 'svg' | 'canvas' | factory into a factory. |
acquireGoopOverlay(doc, opts), acquireEscapeLayer, goopOverlayCount() | The shared page overlay. |
gooCanvasBackend(), gooCanvasRefCount() | Which canvas backend is live, for tests and devtools. |
GoopRenderer, GoopFinishRenderer | The interfaces a custom renderer implements. Parts carry a data-goop attribute (GOOP_PART_ATTR) so tests and devtools can find them. |
Component engines
attachGoop is the engine behind Button. Every other component has its own attach* function in core, with the same shape: pass the root element and options, get a handle with update(opts?) and destroy(). The Vue components call these on mount, so you only need them to build your own component or an adapter for another framework.
import { attachSlider } from '@goopui/core'
const slider = attachSlider(rootEl, { jelly: true, preset: 'wild' })
slider.update({ speed: 0.5 })
slider.destroy()
| Function | Host | Component |
|---|---|---|
attachSpinner(host, opts) | the spinner element | Spinner |
attachSwitch(host, opts) | the switch root | Switch |
attachCheckbox(host, opts) | the checkbox root | Checkbox |
attachRadioGroup(host, opts) | the radio group root | Radio group |
attachSlider(host, opts) | the slider root | Slider |
attachProgress(host, opts) | the progress root | Progress |
attachEmerge(host, opts) | the panel (anchor option: what it oozes from) | Popover, Dropdown menu, Tooltip |
attachMenuHighlight(container, opts) | the menu content | highlight in Dropdown menu |
attachTabsIndicator(list, opts) | the tab list | Tabs |
attachToaster(root, opts) | vue-sonner's or Sonner's toaster root | Toaster |
Most of them take the same few options: speed, preset, renderer, and reducedMotion, plus a boolean or 0 to 2 strength for each of the component's effects (listed in its *_EFFECTS export, such as SLIDER_EFFECTS or TOASTER_EFFECTS). Each also exports its per-preset feel table (SLIDER_FEEL, PROGRESS_FEEL, TOAST_FEEL, ...), its pure pose functions (sliderPose, progressPose, toastDropPose, ...), and its option, handle, and pose types.
The ones with a color variable paint with it: --goop-slider-color, --goop-progress-color, --goop-switch-fill, --goop-switch-thumb, --goop-checkbox-fill, --goop-radio-fill.
Slider, progress, and toaster
attachSlider(root, options?: SliderOptions): SliderHandle
attachProgress(root, options?: ProgressOptions): ProgressHandle
attachToaster(root, options?: ToasterOptions): ToasterHandle
| Function | Options | Handle |
|---|---|---|
attachSlider | jelly, slosh, stretch, splat, orientation, plus thumbSelector, rangeSelector, trackSelector (they default to shadcn's data-slots) | update, measure(), pose, running, reducedMotion, destroy |
attachProgress | value (null is indeterminate), max, slosh, drips, wobble, seed | update, resize(), pose, running, reducedMotion, destroy |
attachToaster | dropIn, merge, drips | update, refresh(), layer, count, reducedMotion, destroy |
attachSlider and attachProgress add one aria-hidden goo layer to the root and measure the real range, thumbs, or indicator, which you make see-through. attachToaster is different: it keeps one shared layer in document.body behind the whole stack, and moves each toast's background, border, and shadow onto it (TOAST_SELECTOR, TOAST_GOO_ATTR). The toasts stay real DOM.
Shared pieces
| Export | Description |
|---|---|
gooBridge, splatCurve, splatAxes, splashDrops, splatToggleEnd, GOO_SPLAT, gooEffectValue | The splat, bridge, and droplet math the toggles share. |
createGooToggle, GOO_PENDING_ATTR | A runner for one-shot toggle animations. Sets data-goop-pending while the goo hides the real indicator. |
liquidBar, hangingDrip, createSlosh, stepSlosh, sloshWave, LIQUID_SLOSH | Liquid fill, hanging drip, and slosh spring helpers behind Slider and Progress. |
emergeJelly, emergePose, EMERGE_FEEL, emergeMomentum, ... | The emerge engine's pose math and feel tables. |
resolveSurfaceColors, parseCssColor, compositeColor, pageBackdrop | Read a surface's real color, composited over the page, for panels with translucent backgrounds. |
TOOLTIP_FEEL, TABS_FEEL, MENU_FEEL | Per-preset feel for each emerge-based component. |
Reduced motion
Under reduced motion every element switches to a still mode, whatever the preset. The body shape and resting drips stay (without breathing). The drip simulation, wobble, momentum, flow, reach, taffy, label ride, scroll slosh, splashes, shockwaves, and puddles stop, and the oil sheen holds still. Each variable's own behavior is in its reducedMotion note in GOOP_VAR_META. The engine watches the setting live, so turning it on mid-session clears anything in flight.
| Export | Description |
|---|---|
isReducedMotion(doc) | One-time check: OS setting or <html data-reduced-motion="true">. |
watchReducedMotion(doc, cb) | Subscribe to changes. The callback only fires on a change. Returns an unsubscribe function. |
Animation loop and input
Every instance shares one requestAnimationFrame loop. It stops when nothing is moving, and a single frame step is capped, so switching tabs doesn't make things jump.
| Export | Description |
|---|---|
setGoopTimeScale(scale) | Slow every goo on the page down. For one element, use the timeScale option instead. |
isLoopRunning(), activeTickerCount(), loopStats() | For tests and devtools. |
wakeTicker(t) / removeTicker(t) | Low level: add or remove a Ticker ((dt) => boolean, which returns true while it's still moving). |
trackPointer, trackScroll, trackTilt, requestTiltPermission() | The shared input trackers behind momentum, scroll slosh, and tilt. |
Filter
The SVG renderer keeps one hidden <svg> per document, holding a <filter> for each distinct parameter set. Each filter is ref-counted and removed when its last user goes away. The pipeline blurs the source, crushes the alpha into a hard edge, and draws the original pixels on top so colors come from the source.
| Export | Description |
|---|---|
acquireGooFilter(doc, params) | Get or create the filter for { blur, contrast, threshold } and take a reference. Returns { id, release() }. |
gooFilterMarkup(params, id?) | The filter's markup as a string, for SSR or snapshot tests. |
gooFilterId(params), gooColorMatrix(params), gooVanishRadius | Helpers. |
createOilFilter, OIL_PALETTE | The oil finish's 2D-fallback filter and palette. |
Low-level physics
These are exported for adapters and tests. Most apps never need them.
| Export | Description |
|---|---|
DripField | The drip simulation: spawning, swelling, necking, detaching, falling, and resting drips. |
ParticleFluid, fluidParams, drainRadius, liveParticleCount | The particle fluid behind splashes and shockwaves. |
springConfig(feel, viscosity?) | Turn { response, dampingRatio } into stiffness and damping. |
stepSpring, stepFall, isSpringSettled, clampDt | Closed-form spring and fall steps. Frame-rate independent. |