A soft glass-morphism button that squishes on press — feels like poking a gummy bear
Click and hold to see the squish effect
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Supports hover, press, and release states with smooth transitions. Use the stretch prop to make buttons wider.
"use client";
import React, { useState } from "react";
export interface SquishyButtonProps {
/** Button label text */
label?: string;
/** Text color (default: hsla(0, 0%, 20%, 1)) */
textColor?: string;
/** Glass overlay opacity (0-1, default 0.18) */
glassOpacity?: number;
/** Show dotted pattern background */
showDots?: boolean;
/** Dot color */
dotColor?: string;
/** Dot spacing in pixels (default 28) */
dotGap?: number;
/** Container width */
containerWidth?: number;
/** Container height */
containerHeight?: number;
/** Container background color */
backgroundColor?: string;
/** Font size base (controls overall button scale) */
fontSize?: string;
/** Horizontal stretch multiplier (default 1, use 1.3 for 30% wider) */
stretch?: number;
/** onClick handler */
onClick?: () => void;
/** Optional className for container */
className?: string;
}
export default function SquishyButton({
label = "Generate",
textColor = "hsla(0, 0%, 20%, 1)",
glassOpacity = 0.18,
showDots = true,
dotColor = "hsla(0, 0%, 0%, 0.12)",
dotGap = 28,
containerWidth = 400,
containerHeight = 300,
backgroundColor = "hsla(0, 0%, 84%, 1)",
fontSize = "clamp(2rem, 4vw, 5rem)",
stretch = 1,
onClick,
className = "",
}: SquishyButtonProps) {
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
// Generate unique pattern ID
const patternId = React.useRef(
`dot-pattern-${Math.random().toString(36).substr(2, 9)}`
);
// Animation timing
const animDuration = "380ms";
const animEasing = "cubic-bezier(0.22, 1, 0.36, 1)";
// Computed glass tint
const glassTint = `hsla(0, 0%, 100%, ${glassOpacity})`;
// Horizontal padding with stretch
const paddingX = `${1.5 * stretch}em`;
return (
<div
className={`relative overflow-hidden flex items-center justify-center ${className}`}
style={{
width: containerWidth,
height: containerHeight,
backgroundColor,
fontFamily: "'Inter', sans-serif",
fontSize,
}}
>
{/* Dotted Pattern Background */}
{showDots && (
<svg
style={{
position: "absolute",
width: "100%",
height: "100%",
zIndex: 0,
}}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<pattern
id={patternId.current}
width={dotGap}
height={dotGap}
patternUnits="userSpaceOnUse"
>
<circle cx="2" cy="2" r="1" fill={dotColor} />
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${patternId.current})`} />
</svg>
)}
{/* Button Wrapper */}
<div
className="relative"
style={{
zIndex: 2,
borderRadius: "999vw",
background: "transparent",
pointerEvents: "none",
transition: `all ${animDuration} ${animEasing}`,
transform: isPressed ? "rotate3d(1, 0, 0, 22deg)" : "none",
}}
>
{/* Drop Shadow */}
<div
style={{
position: "absolute",
width: "calc(100% + 2em)",
height: "calc(100% + 2em)",
top: "-1em",
left: "-1em",
filter: isHovered
? "blur(clamp(2px, 0.06em, 5px))"
: "blur(clamp(2px, 0.12em, 10px))",
overflow: "visible",
pointerEvents: "none",
transition: `filter ${animDuration} ${animEasing}`,
}}
>
<div
style={{
position: "absolute",
zIndex: 0,
borderRadius: "999vw",
background:
"linear-gradient(180deg, hsla(0, 0%, 0%, 0.18), hsla(0, 0%, 0%, 0.08))",
width: "calc(100% - 2em - 0.25em)",
height: "calc(100% - 2em - 0.25em)",
top: isPressed
? "calc(2em - 0.5em)"
: isHovered
? "calc(2em - 0.9em)"
: "calc(2em - 0.5em)",
left: "calc(2em - 0.9em)",
padding: "0.12em",
boxSizing: "border-box",
mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
maskComposite: "exclude",
WebkitMask:
"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
WebkitMaskComposite: "xor",
transition: `all ${animDuration} ${animEasing}`,
opacity: isPressed ? 0.7 : 1,
}}
/>
</div>
{/* Glass Button */}
<button
onClick={onClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => {
setIsHovered(false);
setIsPressed(false);
}}
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
style={{
all: "unset",
cursor: "pointer",
position: "relative",
WebkitTapHighlightColor: "transparent",
pointerEvents: "auto",
zIndex: 3,
background: `linear-gradient(-72deg, ${glassTint}, hsla(0, 0%, 100%, 0.22), ${glassTint})`,
borderRadius: "999vw",
boxShadow: isPressed
? `inset 0 0.12em 0.12em hsla(0, 0%, 0%, 0.05),
inset 0 -0.12em 0.12em hsla(0, 0%, 100%, 0.48),
0 0.12em 0.12em -0.12em hsla(0, 0%, 0%, 0.18),
0 0 0.1em 0.24em inset hsla(0, 0%, 100%, 0.18),
0 0.22em 0.05em 0 hsla(0, 0%, 0%, 0.05),
0 0.24em 0 0 hsla(0, 0%, 100%, 0.72),
inset 0 0.24em 0.05em 0 hsla(0, 0%, 0%, 0.14)`
: isHovered
? `inset 0 0.12em 0.12em hsla(0, 0%, 0%, 0.05),
inset 0 -0.12em 0.12em hsla(0, 0%, 100%, 0.48),
0 0.14em 0.05em -0.1em hsla(0, 0%, 0%, 0.22),
0 0 0.05em 0.1em inset hsla(0, 0%, 100%, 0.48)`
: `inset 0 0.12em 0.12em hsla(0, 0%, 0%, 0.05),
inset 0 -0.12em 0.12em hsla(0, 0%, 100%, 0.48),
0 0.24em 0.12em -0.12em hsla(0, 0%, 0%, 0.18),
0 0 0.1em 0.24em inset hsla(0, 0%, 100%, 0.18)`,
backdropFilter: isHovered
? "blur(0.01em)"
: "blur(clamp(1px, 0.12em, 4px))",
WebkitBackdropFilter: isHovered
? "blur(0.01em)"
: "blur(clamp(1px, 0.12em, 4px))",
transition: `all ${animDuration} ${animEasing}`,
transform: isHovered && !isPressed ? "scale(0.978)" : "scale(1)",
}}
>
{/* Label */}
<span
style={{
position: "relative",
display: "block",
userSelect: "none",
WebkitUserSelect: "none",
fontFamily: "'Inter', sans-serif",
letterSpacing: "-0.04em",
fontWeight: 500,
fontSize: "1em",
color: textColor,
textShadow: isHovered
? "0.02em 0.02em 0.02em hsla(0, 0%, 0%, 0.1)"
: isPressed
? "0.02em 0.24em 0.05em hsla(0, 0%, 0%, 0.1)"
: "0em 0.24em 0.05em hsla(0, 0%, 0%, 0.08)",
transition: `all ${animDuration} ${animEasing}`,
paddingInline: paddingX,
paddingBlock: "0.85em",
}}
>
{label}
{/* Shine Effect */}
<span
style={{
display: "block",
position: "absolute",
zIndex: 1,
width: "calc(100% - clamp(1px, 0.06em, 4px))",
height: "calc(100% - clamp(1px, 0.06em, 4px))",
top: "calc(clamp(1px, 0.06em, 4px) / 2)",
left: "calc(clamp(1px, 0.06em, 4px) / 2)",
boxSizing: "border-box",
borderRadius: "999vw",
overflow: "clip",
background: `linear-gradient(
${isPressed ? "-18deg" : "-42deg"},
hsla(0, 0%, 100%, 0) 0%,
hsla(0, 0%, 100%, 0.48) 38% 52%,
hsla(0, 0%, 100%, 0) 58%
)`,
mixBlendMode: "screen",
pointerEvents: "none",
backgroundSize: "200% 200%",
backgroundPosition: isPressed
? "48% 18%"
: isHovered
? "22% 50%"
: "0% 50%",
backgroundRepeat: "no-repeat",
transition: `background-position calc(${animDuration} * 1.2) ${animEasing}`,
}}
/>
</span>
{/* Edge Highlight */}
<span
style={{
position: "absolute",
zIndex: 1,
inset: 0,
borderRadius: "999vw",
width: "calc(100% + clamp(1px, 0.06em, 4px))",
height: "calc(100% + clamp(1px, 0.06em, 4px))",
top: "calc(-1 * clamp(1px, 0.06em, 4px) / 2)",
left: "calc(-1 * clamp(1px, 0.06em, 4px) / 2)",
padding: "clamp(1px, 0.06em, 4px)",
boxSizing: "border-box",
background: `conic-gradient(
from ${isPressed ? "-72deg" : isHovered ? "-128deg" : "-72deg"} at 50% 50%,
hsla(0, 0%, 0%, 0.48),
hsla(0, 0%, 0%, 0) 6% 42%,
hsla(0, 0%, 0%, 0.48) 50%,
hsla(0, 0%, 0%, 0) 58% 94%,
hsla(0, 0%, 0%, 0.48)
),
linear-gradient(180deg, hsla(0, 0%, 100%, 0.48), hsla(0, 0%, 100%, 0.48))`,
mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
maskComposite: "exclude",
WebkitMask:
"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
WebkitMaskComposite: "xor",
transition: `all ${animDuration} ${animEasing}`,
boxShadow:
"inset 0 0 0 calc(clamp(1px, 0.06em, 4px) / 2) hsla(0, 0%, 100%, 0.48)",
pointerEvents: "none",
}}
/>
</button>
</div>
</div>
);
}
export { SquishyButton };// BASIC: Minimal setup
import SquishyButton from "@/components/SquishyButton";
export default function MyComponent() {
return (
<SquishyButton label="Click Me" />
);
}
// WIDER BUTTON (30% stretch)
export function WideButton() {
return (
<SquishyButton
label="Submit"
stretch={1.3}
onClick={() => alert("Submitted!")}
/>
);
}
// CUSTOM COLORS
export function ColorfulButton() {
return (
<SquishyButton
label="Create"
textColor="hsla(220, 50%, 25%, 1)"
backgroundColor="hsla(220, 30%, 92%, 1)"
/>
);
}
// NO DOT BACKGROUND
export function CleanButton() {
return (
<SquishyButton
label="Generate"
showDots={false}
backgroundColor="hsla(0, 0%, 94%, 1)"
/>
);
}
// SMALLER SIZE
export function CompactButton() {
return (
<SquishyButton
label="Go"
fontSize="clamp(1rem, 2vw, 2rem)"
containerWidth={200}
containerHeight={150}
/>
);
}
// MORE GLASS EFFECT
export function GlassyButton() {
return (
<SquishyButton
label="Glass"
glassOpacity={0.25}
/>
);
}
// FULL CUSTOMIZATION
export function CustomButton() {
return (
<SquishyButton
label="Launch"
textColor="hsla(0, 0%, 18%, 1)"
glassOpacity={0.2}
showDots={true}
dotColor="hsla(0, 0%, 0%, 0.1)"
dotGap={24}
containerWidth={500}
containerHeight={350}
backgroundColor="hsla(0, 0%, 88%, 1)"
fontSize="clamp(1.5rem, 3vw, 4rem)"
stretch={1.2}
onClick={() => console.log("Launched!")}
/>
);
}