Physics Buttons

v1.1.1
FreeInteractivity

A suite of four physics-driven action buttons — gooey magnetic (slime), elastic bezier, jelly, and impact — built on GSAP spring physics with style presets and deep prop customization.

Physics Buttons

Installation

Step 1. Initialize ProjectOne-time setup

Run once in your project root to configure paths and dependencies:

npx vectorvesper init
Step 2. Add ComponentInstalls source & assets

Downloads and registers the component in your project:

npx vectorvesper add physics-buttons
Step 3. Required DependenciesAuto-installed with CLI

If installing manually or managing your package lock:

npm install gsap @gsap/react
Packages:gsap@gsap/react
Requires Tailwind CSS to be configured in your project.

Usage

Import

import { SlimeButton, ElasticButton, JellyButton, ImpactButton } from "@/components/vv/physics-buttons";

Usage

<SlimeButton />
<ElasticButton />
<JellyButton />
<ImpactButton />
Tailwind CSS must be configured in your project for styles to apply correctly.

Props

Four buttons that behave like materials rather than rectangles. One stretches toward your cursor and snaps back, one wobbles like set jelly, one drops and lands with a shower of particles, and one is soft enough to squash. Each ships with variants.

Shared Props

Common to all exported components in this suite.

PropTypeDefaultDescription
children*React.ReactNodeThe label.
onClick() => voidFires on click, as on a normal button.
disabledbooleanfalseDisables the button and stops the physics with it.
type"button" | "submit" | "reset""button"Native button type. Set it to submit inside a form.
clickColorstring"#FFFFFF"Flash colour on press.
buttonColorstringper variantFill colour. Each variant carries its own; set this to override.
aria-labelstringSet this when the label is an icon or otherwise not descriptive on its own.
classNamestring""Applied to the button.
styleReact.CSSProperties{}Inline styles merged onto the button.

<ElasticButton />

Stretches toward the cursor as it approaches, then snaps back when you leave.

PropTypeDefaultDescription
variant"elastic" | "outlined" | "tension""elastic"Filled, outlined, or a tighter, higher-tension pull.
widthnumber220Width in pixels.
heightnumber60Height in pixels.
rangenumber140How far away the cursor starts pulling on it.
maxStretchnumber28Furthest it will deform, in pixels.
pluckForcenumberper variantHow hard it springs back when released.
borderColorstringper variantEdge colour. Mainly for the outlined variant.
borderWidthnumberper variantEdge thickness.

Presets

PresetDescriptionKey Overrides
"elastic"Solid fill, generous stretch. The default.
"outlined"Edge only, so the deformation reads as a drawn line.
"tension"Stiffer — less travel, faster return.

<JellyButton />

Wobbles and settles, like something set rather than solid.

PropTypeDefaultDescription
variant"jelly" | "viscous""jelly"Quick wobble, or a slower, thicker one.
widthnumber200Width in pixels.
heightnumber56Height in pixels.
rangenumber130How far away the cursor starts affecting it.
radiusnumberper variantCorner radius.
maxStretchnumberper variantFurthest it will deform.
rippleForcenumberper variantStrength of the wobble that travels through it after a press.

Presets

PresetDescriptionKey Overrides
"jelly"Fast wobble that settles quickly. The default.
"viscous"Thicker and slower — the wobble takes longer to die out.

<ImpactButton />

Falls into place on mount and throws off particles where it lands.

PropTypeDefaultDescription
variant"impact" | "drop" | "sticky""impact"How it arrives and what it does on landing.
widthnumber200Width in pixels.
heightnumber56Height in pixels.
fallDistancenumberper variantHow far above its resting place it starts.
impactDelayMsnumberper variantPause before the fall begins, in milliseconds. Stagger a row of them with this.
particleCountnumberper variantHow many particles the landing throws.
particleColorstringthe button colourColour of those particles.
minRadiusnumberper variantSmallest particle size.
maxRadiusnumberper variantLargest particle size.
borderRadiusstringper variantCorner radius, as any CSS length.

Presets

PresetDescriptionKey Overrides
"impact"Lands hard and scatters. The default.
"drop"A softer arrival with fewer particles.
"sticky"Lands and clings, deforming on contact.

<SlimeButton />

Soft enough to squash under the cursor, with particles orbiting it.

PropTypeDefaultDescription
variant"slime" | "orbit" | "square""slime"Blobby, orbiting particles, or a squared-off version.
minWidthstring"160px"Minimum width, as any CSS length. It grows to fit its label.
heightstring"56px"Height, as any CSS length.
rangenumber150How far away the cursor starts affecting it.
strengthnumber0.3How strongly it reacts within that range.
squishIntensitynumber-0.35How far it flattens on press. Negative squashes; positive would stretch.
particleCountnumberper variantHow many particles surround it.
particleColorstringthe button colourColour of those particles.
gravitynumberper variantDownward pull on the particles.
haloSpeednumberper variantHow fast they orbit.
releaseTimeSecnumberper variantSeconds to return to rest after release.
recoilIntensitynumberper variantHow far it overshoots on the way back.
stdDeviationnumberper variantBlur that fuses the button and its particles into one blob. Lower separates them.
borderRadiusstringper variantCorner radius, as any CSS length.

Presets

PresetDescriptionKey Overrides
"slime"Round and gooey, particles fused to the body. The default.
"orbit"Particles circle at a distance instead of clinging.
"square"Squared corners, so the squash reads more sharply.

Examples

A button that stretches toward the cursor
import { ElasticButton } from "@/components/vv/physics-buttons/ElasticButton";

<ElasticButton onClick={submit}>Send it</ElasticButton>
A staggered row that drops in
{actions.map((a, i) => (
  <ImpactButton key={a.id} impactDelayMs={i * 120} onClick={a.run}>
    {a.label}
  </ImpactButton>
))}
Softer, in your own colour
<SlimeButton
  variant="orbit"
  buttonColor="#7EACB5"
  particleColor="#E07B9B"
  strength={0.5}
  squishIntensity={-0.5}
>
  Explore
</SlimeButton>
SlimeButton runs an orbit continuously and has no reduced-motion guard; the other three only move in response to you. Render a plain button when the preference is set if the orbit is a concern.
These are real buttons — they take type, disabled and onClick, and work inside a form. Set aria-label when the label is an icon.
The deformation is a filter over the button, which is inexpensive for a handful of them. A long list of physics buttons on one screen is not what they are for.

Source

/**
 * Physics Buttons — Vector Vesper
 * https://vectorvesper.dev/components
 *
 * Copyright (c) 2026 Vector Vesper
 * Released under the MIT License. This notice must be retained in copies and
 * substantial portions of the file. https://vectorvesper.dev/license
 */
export { default as SlimeButton } from "./SlimeButton";
export type { SlimeButtonProps } from "./SlimeButton";

export { default as ElasticButton } from "./ElasticButton";
export type { ElasticButtonProps } from "./ElasticButton";

export { default as ImpactButton } from "./ImpactButton";
export type { ImpactButtonProps } from "./ImpactButton";

export { default as JellyButton } from "./JellyButton";
export type { JellyButtonProps } from "./JellyButton";
View on GitHubReport an issue